mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-01 14:34:24 +02:00
🔮 Merge repository updated to latest snapshot!
Script Execution UTC Time: null Signed-off-by: validcube <pun.butrach@gmail.com>
This commit is contained in:
commit
177b716fd0
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,3 +9,4 @@
|
|||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
|
|
||||||
|
.kotlin/
|
||||||
|
@ -74,7 +74,7 @@ special contributor role.
|
|||||||
### ⏳ Supported Versions
|
### ⏳ Supported Versions
|
||||||
|
|
||||||
| Version | Branch | Supported |
|
| Version | Branch | Supported |
|
||||||
| ---------------------------------------- | ------------- | ------------------ |
|
|------------------------------------------|---------------|--------------------|
|
||||||
| ![Latest stable release][LatestRelBadge] | `main` | :white_check_mark: |
|
| ![Latest stable release][LatestRelBadge] | `main` | :white_check_mark: |
|
||||||
| ![Latest version][LatestVerBadge] | `dev` | :white_check_mark: |
|
| ![Latest version][LatestVerBadge] | `dev` | :white_check_mark: |
|
||||||
| ![Latest version][LatestVerBadge] | `compose-dev` | :white_check_mark: |
|
| ![Latest version][LatestVerBadge] | `compose-dev` | :white_check_mark: |
|
||||||
|
@ -3,21 +3,22 @@ import kotlin.random.Random
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
|
alias(libs.plugins.compose.compiler)
|
||||||
alias(libs.plugins.devtools)
|
alias(libs.plugins.devtools)
|
||||||
alias(libs.plugins.about.libraries)
|
alias(libs.plugins.about.libraries)
|
||||||
id("kotlin-parcelize")
|
|
||||||
kotlin("plugin.serialization") version "1.9.23"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.manager"
|
namespace = "app.revanced.manager"
|
||||||
compileSdk = 34
|
compileSdk = 35
|
||||||
buildToolsVersion = "34.0.0"
|
buildToolsVersion = "35.0.0"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "app.revanced.manager"
|
applicationId = "app.revanced.manager"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "0.0.1"
|
versionName = "0.0.1"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
@ -81,9 +82,11 @@ android {
|
|||||||
jvmTarget = "17"
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures.compose = true
|
buildFeatures {
|
||||||
buildFeatures.aidl = true
|
compose = true
|
||||||
buildFeatures.buildConfig=true
|
aidl = true
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
androidResources {
|
androidResources {
|
||||||
@ -91,7 +94,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
composeOptions.kotlinCompilerExtensionVersion = "1.5.10"
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
path = file("src/main/cpp/CMakeLists.txt")
|
path = file("src/main/cpp/CMakeLists.txt")
|
||||||
@ -111,10 +113,10 @@ dependencies {
|
|||||||
implementation(libs.runtime.ktx)
|
implementation(libs.runtime.ktx)
|
||||||
implementation(libs.runtime.compose)
|
implementation(libs.runtime.compose)
|
||||||
implementation(libs.splash.screen)
|
implementation(libs.splash.screen)
|
||||||
implementation(libs.compose.activity)
|
implementation(libs.activity.compose)
|
||||||
implementation(libs.paging.common.ktx)
|
|
||||||
implementation(libs.work.runtime.ktx)
|
implementation(libs.work.runtime.ktx)
|
||||||
implementation(libs.preferences.datastore)
|
implementation(libs.preferences.datastore)
|
||||||
|
implementation(libs.appcompat)
|
||||||
|
|
||||||
// Compose
|
// Compose
|
||||||
implementation(platform(libs.compose.bom))
|
implementation(platform(libs.compose.bom))
|
||||||
@ -124,6 +126,7 @@ dependencies {
|
|||||||
implementation(libs.compose.livedata)
|
implementation(libs.compose.livedata)
|
||||||
implementation(libs.compose.material.icons.extended)
|
implementation(libs.compose.material.icons.extended)
|
||||||
implementation(libs.compose.material3)
|
implementation(libs.compose.material3)
|
||||||
|
implementation(libs.navigation.compose)
|
||||||
|
|
||||||
// Accompanist
|
// Accompanist
|
||||||
implementation(libs.accompanist.drawablepainter)
|
implementation(libs.accompanist.drawablepainter)
|
||||||
@ -142,6 +145,7 @@ dependencies {
|
|||||||
// KotlinX
|
// KotlinX
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.kotlinx.collection.immutable)
|
implementation(libs.kotlinx.collection.immutable)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
|
|
||||||
// Room
|
// Room
|
||||||
implementation(libs.room.runtime)
|
implementation(libs.room.runtime)
|
||||||
@ -153,6 +157,9 @@ dependencies {
|
|||||||
implementation(libs.revanced.patcher)
|
implementation(libs.revanced.patcher)
|
||||||
implementation(libs.revanced.library)
|
implementation(libs.revanced.library)
|
||||||
|
|
||||||
|
// Downloader plugins
|
||||||
|
implementation(project(":downloader-plugin"))
|
||||||
|
|
||||||
// Native processes
|
// Native processes
|
||||||
implementation(libs.kotlin.process)
|
implementation(libs.kotlin.process)
|
||||||
|
|
||||||
@ -167,11 +174,9 @@ dependencies {
|
|||||||
// Koin
|
// Koin
|
||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
|
implementation(libs.koin.compose.navigation)
|
||||||
implementation(libs.koin.workmanager)
|
implementation(libs.koin.workmanager)
|
||||||
|
|
||||||
// Compose Navigation
|
|
||||||
implementation(libs.reimagined.navigation)
|
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
implementation(libs.about.libraries)
|
implementation(libs.about.libraries)
|
||||||
|
|
||||||
|
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
@ -49,6 +49,10 @@
|
|||||||
-keep class com.android.** {
|
-keep class com.android.** {
|
||||||
*;
|
*;
|
||||||
}
|
}
|
||||||
|
-keep class app.revanced.manager.plugin.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
-dontwarn com.google.auto.value.**
|
-dontwarn com.google.auto.value.**
|
||||||
-dontwarn java.awt.**
|
-dontwarn java.awt.**
|
||||||
-dontwarn javax.**
|
-dontwarn javax.**
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "1dd9d5c0201fdf3cfef3ae669fd65e46",
|
"identityHash": "d0119047505da435972c5247181de675",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "patch_bundles",
|
"tableName": "patch_bundles",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, `version` TEXT, `integrations_version` TEXT, PRIMARY KEY(`uid`))",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` TEXT, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, PRIMARY KEY(`uid`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "uid",
|
"fieldPath": "uid",
|
||||||
@ -20,6 +20,12 @@
|
|||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "source",
|
"fieldPath": "source",
|
||||||
"columnName": "source",
|
"columnName": "source",
|
||||||
@ -31,18 +37,6 @@
|
|||||||
"columnName": "auto_update",
|
"columnName": "auto_update",
|
||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "versionInfo.patches",
|
|
||||||
"columnName": "version",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "versionInfo.integrations",
|
|
||||||
"columnName": "integrations_version",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
@ -150,7 +144,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "downloaded_app",
|
"tableName": "downloaded_app",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, PRIMARY KEY(`package_name`, `version`))",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, `last_used` INTEGER NOT NULL, PRIMARY KEY(`package_name`, `version`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "packageName",
|
"fieldPath": "packageName",
|
||||||
@ -169,6 +163,12 @@
|
|||||||
"columnName": "directory",
|
"columnName": "directory",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUsed",
|
||||||
|
"columnName": "last_used",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
@ -392,12 +392,38 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "trusted_downloader_plugins",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "signature",
|
||||||
|
"columnName": "signature",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"package_name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"views": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1dd9d5c0201fdf3cfef3ae669fd65e46')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd0119047505da435972c5247181de675')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,9 +2,16 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
<permission
|
||||||
tools:ignore="ReservedSystemPermission" />
|
android:name="app.revanced.manager.permission.PLUGIN_HOST"
|
||||||
|
android:protectionLevel="signature"
|
||||||
|
android:label="@string/plugin_host_permission_label"
|
||||||
|
android:description="@string/plugin_host_permission_description"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<uses-permission android:name="app.revanced.manager.permission.PLUGIN_HOST" />
|
||||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
@ -17,12 +24,6 @@
|
|||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<queries>
|
|
||||||
<intent>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
</intent>
|
|
||||||
</queries>
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".ManagerApplication"
|
android:name=".ManagerApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@ -47,6 +48,8 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||||
|
|
||||||
<service android:name=".service.InstallService" />
|
<service android:name=".service.InstallService" />
|
||||||
<service android:name=".service.UninstallService" />
|
<service android:name=".service.UninstallService" />
|
||||||
|
|
||||||
|
@ -1,150 +1,307 @@
|
|||||||
package app.revanced.manager
|
package app.revanced.manager
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import app.revanced.manager.ui.destination.Destination
|
import androidx.core.view.WindowCompat
|
||||||
import app.revanced.manager.ui.destination.SettingsDestination
|
import androidx.lifecycle.lifecycleScope
|
||||||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
import androidx.navigation.NavBackStackEntry
|
||||||
import app.revanced.manager.ui.screen.DashboardScreen
|
import androidx.navigation.NavController
|
||||||
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
|
import androidx.navigation.compose.NavHost
|
||||||
import app.revanced.manager.ui.screen.PatcherScreen
|
import androidx.navigation.compose.composable
|
||||||
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
import androidx.navigation.compose.navigation
|
||||||
import app.revanced.manager.ui.screen.SettingsScreen
|
import androidx.navigation.compose.rememberNavController
|
||||||
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
import androidx.navigation.toRoute
|
||||||
|
import app.revanced.manager.ui.model.navigation.*
|
||||||
|
import app.revanced.manager.ui.screen.*
|
||||||
|
import app.revanced.manager.ui.screen.settings.*
|
||||||
|
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
|
||||||
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
||||||
import app.revanced.manager.ui.theme.Theme
|
import app.revanced.manager.ui.theme.Theme
|
||||||
import app.revanced.manager.ui.viewmodel.MainViewModel
|
import app.revanced.manager.ui.viewmodel.MainViewModel
|
||||||
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||||
import dev.olshevski.navigation.reimagined.AnimatedNavHost
|
import app.revanced.manager.util.EventEffect
|
||||||
import dev.olshevski.navigation.reimagined.NavBackHandler
|
import kotlinx.coroutines.launch
|
||||||
import dev.olshevski.navigation.reimagined.navigate
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import dev.olshevski.navigation.reimagined.pop
|
import org.koin.androidx.compose.navigation.koinNavViewModel
|
||||||
import dev.olshevski.navigation.reimagined.popUpTo
|
|
||||||
import dev.olshevski.navigation.reimagined.rememberNavController
|
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import org.koin.androidx.compose.koinViewModel as getComposeViewModel
|
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
enableEdgeToEdge()
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
|
|
||||||
val vm: MainViewModel = getAndroidViewModel()
|
val vm: MainViewModel = getActivityViewModel()
|
||||||
vm.importLegacySettings(this)
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
|
onResult = vm::applyLegacySettings
|
||||||
|
)
|
||||||
val theme by vm.prefs.theme.getAsState()
|
val theme by vm.prefs.theme.getAsState()
|
||||||
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||||
|
|
||||||
|
EventEffect(vm.legacyImportActivityFlow) {
|
||||||
|
try {
|
||||||
|
launcher.launch(it)
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ReVancedManagerTheme(
|
ReVancedManagerTheme(
|
||||||
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
||||||
dynamicColor = dynamicColor
|
dynamicColor = dynamicColor
|
||||||
) {
|
) {
|
||||||
val navController =
|
ReVancedManager(vm)
|
||||||
rememberNavController<Destination>(startDestination = Destination.Dashboard)
|
|
||||||
NavBackHandler(navController)
|
|
||||||
|
|
||||||
AnimatedNavHost(
|
|
||||||
controller = navController
|
|
||||||
) { destination ->
|
|
||||||
when (destination) {
|
|
||||||
is Destination.Dashboard -> DashboardScreen(
|
|
||||||
onSettingsClick = { navController.navigate(Destination.Settings()) },
|
|
||||||
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
|
|
||||||
onUpdateClick = { navController.navigate(
|
|
||||||
Destination.Settings(SettingsDestination.Update())
|
|
||||||
) },
|
|
||||||
onAppClick = { installedApp ->
|
|
||||||
navController.navigate(
|
|
||||||
Destination.InstalledApplicationInfo(
|
|
||||||
installedApp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen(
|
|
||||||
onPatchClick = { packageName, patchSelection ->
|
|
||||||
navController.navigate(
|
|
||||||
Destination.VersionSelector(
|
|
||||||
packageName,
|
|
||||||
patchSelection
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onBackClick = { navController.pop() },
|
|
||||||
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
|
|
||||||
)
|
|
||||||
|
|
||||||
is Destination.Settings -> SettingsScreen(
|
|
||||||
onBackClick = { navController.pop() },
|
|
||||||
startDestination = destination.startDestination
|
|
||||||
)
|
|
||||||
|
|
||||||
is Destination.AppSelector -> AppSelectorScreen(
|
|
||||||
onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
|
|
||||||
onStorageClick = {
|
|
||||||
navController.navigate(
|
|
||||||
Destination.SelectedApplicationInfo(
|
|
||||||
it
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onBackClick = { navController.pop() }
|
|
||||||
)
|
|
||||||
|
|
||||||
is Destination.VersionSelector -> VersionSelectorScreen(
|
|
||||||
onBackClick = { navController.pop() },
|
|
||||||
onAppClick = { selectedApp ->
|
|
||||||
navController.navigate(
|
|
||||||
Destination.SelectedApplicationInfo(
|
|
||||||
selectedApp,
|
|
||||||
destination.patchSelection,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
viewModel = getComposeViewModel {
|
|
||||||
parametersOf(
|
|
||||||
destination.packageName,
|
|
||||||
destination.patchSelection
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
|
|
||||||
onPatchClick = { app, patches, options ->
|
|
||||||
navController.navigate(
|
|
||||||
Destination.Patcher(
|
|
||||||
app, patches, options
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onBackClick = navController::pop,
|
|
||||||
vm = getComposeViewModel {
|
|
||||||
parametersOf(
|
|
||||||
SelectedAppInfoViewModel.Params(
|
|
||||||
destination.selectedApp,
|
|
||||||
destination.patchSelection
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
is Destination.Patcher -> PatcherScreen(
|
|
||||||
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
|
|
||||||
vm = getComposeViewModel { parametersOf(destination) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReVancedManager(vm: MainViewModel) {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
EventEffect(vm.appSelectFlow) { app ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedApplicationInfo,
|
||||||
|
SelectedApplicationInfo.ViewModelParams(app)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = Dashboard,
|
||||||
|
) {
|
||||||
|
composable<Dashboard> {
|
||||||
|
DashboardScreen(
|
||||||
|
onSettingsClick = { navController.navigate(Settings) },
|
||||||
|
onAppSelectorClick = {
|
||||||
|
navController.navigate(AppSelector)
|
||||||
|
},
|
||||||
|
onUpdateClick = {
|
||||||
|
navController.navigate(Update())
|
||||||
|
},
|
||||||
|
onDownloaderPluginClick = {
|
||||||
|
navController.navigate(Settings.Downloads)
|
||||||
|
},
|
||||||
|
onAppClick = { packageName ->
|
||||||
|
navController.navigate(InstalledApplicationInfo(packageName))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<InstalledApplicationInfo> {
|
||||||
|
val data = it.toRoute<InstalledApplicationInfo>()
|
||||||
|
|
||||||
|
InstalledAppInfoScreen(
|
||||||
|
onPatchClick = vm::selectApp,
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
viewModel = koinViewModel { parametersOf(data.packageName) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<AppSelector> {
|
||||||
|
AppSelectorScreen(
|
||||||
|
onSelect = vm::selectApp,
|
||||||
|
onStorageSelect = vm::selectApp,
|
||||||
|
onBackClick = navController::popBackStack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Patcher> {
|
||||||
|
PatcherScreen(
|
||||||
|
onBackClick = {
|
||||||
|
navController.navigate(route = Dashboard) {
|
||||||
|
launchSingleTop = true
|
||||||
|
popUpTo<Dashboard> {
|
||||||
|
inclusive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
vm = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Update> {
|
||||||
|
val data = it.toRoute<Update>()
|
||||||
|
|
||||||
|
UpdateScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
vm = koinViewModel { parametersOf(data.downloadOnScreenEntry) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
|
||||||
|
composable<SelectedApplicationInfo.Main> {
|
||||||
|
val parentBackStackEntry = navController.navGraphEntry(it)
|
||||||
|
val data =
|
||||||
|
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
||||||
|
val viewModel =
|
||||||
|
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||||
|
parametersOf(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedAppInfoScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onPatchClick = {
|
||||||
|
it.lifecycleScope.launch {
|
||||||
|
navController.navigateComplex(
|
||||||
|
Patcher,
|
||||||
|
viewModel.getPatcherParams()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPatchSelectorClick = { app, patches, options ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedApplicationInfo.PatchesSelector,
|
||||||
|
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||||
|
app,
|
||||||
|
patches,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onRequiredOptions = { app, patches, options ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedApplicationInfo.RequiredOptions,
|
||||||
|
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||||
|
app,
|
||||||
|
patches,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
vm = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<SelectedApplicationInfo.PatchesSelector> {
|
||||||
|
val data =
|
||||||
|
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||||
|
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||||
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
|
)
|
||||||
|
|
||||||
|
PatchesSelectorScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onSave = { patches, options ->
|
||||||
|
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
vm = koinViewModel { parametersOf(data) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<SelectedApplicationInfo.RequiredOptions> {
|
||||||
|
val data =
|
||||||
|
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||||
|
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||||
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
|
)
|
||||||
|
|
||||||
|
RequiredOptionsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onContinue = { patches, options ->
|
||||||
|
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||||
|
it.lifecycleScope.launch {
|
||||||
|
navController.navigateComplex(
|
||||||
|
Patcher,
|
||||||
|
selectedAppInfoVm.getPatcherParams()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
vm = koinViewModel { parametersOf(data) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigation<Settings>(startDestination = Settings.Main) {
|
||||||
|
composable<Settings.Main> {
|
||||||
|
SettingsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
navigate = navController::navigate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.General> {
|
||||||
|
GeneralSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Advanced> {
|
||||||
|
AdvancedSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Updates> {
|
||||||
|
UpdatesSettingsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onChangelogClick = { navController.navigate(Settings.Changelogs) },
|
||||||
|
onUpdateClick = { navController.navigate(Update()) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Downloads> {
|
||||||
|
DownloadsSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.ImportExport> {
|
||||||
|
ImportExportSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.About> {
|
||||||
|
AboutSettingsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
navigate = navController::navigate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Changelogs> {
|
||||||
|
ChangelogsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Contributors> {
|
||||||
|
ContributorScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Licenses> {
|
||||||
|
LicensesScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.DeveloperOptions> {
|
||||||
|
DeveloperOptionsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavController.navGraphEntry(entry: NavBackStackEntry) =
|
||||||
|
remember(entry) { getBackStackEntry(entry.destination.parent!!.id) }
|
||||||
|
|
||||||
|
// Androidx Navigation does not support storing complex types in route objects, so we have to store them inside the saved state handle of the back stack entry instead.
|
||||||
|
private fun <T : Parcelable, R : ComplexParameter<T>> NavController.navigateComplex(
|
||||||
|
route: R,
|
||||||
|
data: T
|
||||||
|
) {
|
||||||
|
navigate(route)
|
||||||
|
getBackStackEntry(route).savedStateHandle["args"] = data
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T : Parcelable> NavBackStackEntry.getComplexArg() = savedStateHandle.get<T>("args")!!
|
@ -1,9 +1,15 @@
|
|||||||
package app.revanced.manager
|
package app.revanced.manager
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.di.*
|
import app.revanced.manager.di.*
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
@ -23,6 +29,9 @@ class ManagerApplication : Application() {
|
|||||||
private val scope = MainScope()
|
private val scope = MainScope()
|
||||||
private val prefs: PreferencesManager by inject()
|
private val prefs: PreferencesManager by inject()
|
||||||
private val patchBundleRepository: PatchBundleRepository by inject()
|
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||||
|
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
|
||||||
|
private val fs: Filesystem by inject()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
@ -59,11 +68,43 @@ class ManagerApplication : Application() {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
prefs.preload()
|
prefs.preload()
|
||||||
}
|
}
|
||||||
|
scope.launch(Dispatchers.Default) {
|
||||||
|
downloaderPluginRepository.reload()
|
||||||
|
}
|
||||||
scope.launch(Dispatchers.Default) {
|
scope.launch(Dispatchers.Default) {
|
||||||
with(patchBundleRepository) {
|
with(patchBundleRepository) {
|
||||||
reload()
|
reload()
|
||||||
updateCheck()
|
updateCheck()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||||
|
private var firstActivityCreated = false
|
||||||
|
|
||||||
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||||
|
if (firstActivityCreated) return
|
||||||
|
firstActivityCreated = true
|
||||||
|
|
||||||
|
// We do not want to call onFreshProcessStart() if there is state to restore.
|
||||||
|
// This can happen on system-initiated process death.
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
Log.d(tag, "Fresh process created")
|
||||||
|
onFreshProcessStart()
|
||||||
|
} else Log.d(tag, "System-initiated process death detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityStarted(activity: Activity) {}
|
||||||
|
override fun onActivityResumed(activity: Activity) {}
|
||||||
|
override fun onActivityPaused(activity: Activity) {}
|
||||||
|
override fun onActivityStopped(activity: Activity) {}
|
||||||
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||||
|
override fun onActivityDestroyed(activity: Activity) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFreshProcessStart() {
|
||||||
|
fs.uiTempDir.apply {
|
||||||
|
deleteRecursively()
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,6 +9,8 @@ import android.os.Environment
|
|||||||
import androidx.activity.result.contract.ActivityResultContract
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import app.revanced.manager.util.RequestManageStorageContract
|
import app.revanced.manager.util.RequestManageStorageContract
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
class Filesystem(private val app: Application) {
|
class Filesystem(private val app: Application) {
|
||||||
val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here.
|
val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here.
|
||||||
@ -17,21 +19,33 @@ class Filesystem(private val app: Application) {
|
|||||||
* A directory that gets cleared when the app restarts.
|
* A directory that gets cleared when the app restarts.
|
||||||
* Do not store paths to this directory in a parcel.
|
* Do not store paths to this directory in a parcel.
|
||||||
*/
|
*/
|
||||||
val tempDir = app.getDir("ephemeral", Context.MODE_PRIVATE).apply {
|
val tempDir: File = app.getDir("ephemeral", Context.MODE_PRIVATE).apply {
|
||||||
deleteRecursively()
|
deleteRecursively()
|
||||||
mkdirs()
|
mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun externalFilesDir() = Environment.getExternalStorageDirectory().toPath()
|
/**
|
||||||
|
* A directory for storing temporary files related to UI.
|
||||||
|
* This is the same as [tempDir], but does not get cleared on system-initiated process death.
|
||||||
|
* Paths to this directory can be safely stored in parcels.
|
||||||
|
*/
|
||||||
|
val uiTempDir: File = app.getDir("ui_ephemeral", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun externalFilesDir(): Path = Environment.getExternalStorageDirectory().toPath()
|
||||||
|
|
||||||
private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
|
||||||
private val storagePermissionName = if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE
|
private val storagePermissionName =
|
||||||
|
if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
|
||||||
fun permissionContract(): Pair<ActivityResultContract<String, Boolean>, String> {
|
fun permissionContract(): Pair<ActivityResultContract<String, Boolean>, String> {
|
||||||
val contract = if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
|
val contract =
|
||||||
|
if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
|
||||||
return contract to storagePermissionName
|
return contract to storagePermissionName
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasStoragePermission() = if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(storagePermissionName) == PackageManager.PERMISSION_GRANTED
|
fun hasStoragePermission() =
|
||||||
|
if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(
|
||||||
|
storagePermissionName
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
}
|
}
|
@ -16,9 +16,14 @@ import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
|||||||
import app.revanced.manager.data.room.options.Option
|
import app.revanced.manager.data.room.options.Option
|
||||||
import app.revanced.manager.data.room.options.OptionDao
|
import app.revanced.manager.data.room.options.OptionDao
|
||||||
import app.revanced.manager.data.room.options.OptionGroup
|
import app.revanced.manager.data.room.options.OptionGroup
|
||||||
|
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
|
||||||
|
import app.revanced.manager.data.room.plugins.TrustedDownloaderPluginDao
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class], version = 1)
|
@Database(
|
||||||
|
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloaderPlugin::class],
|
||||||
|
version = 1
|
||||||
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun patchBundleDao(): PatchBundleDao
|
abstract fun patchBundleDao(): PatchBundleDao
|
||||||
@ -26,6 +31,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
abstract fun downloadedAppDao(): DownloadedAppDao
|
abstract fun downloadedAppDao(): DownloadedAppDao
|
||||||
abstract fun installedAppDao(): InstalledAppDao
|
abstract fun installedAppDao(): InstalledAppDao
|
||||||
abstract fun optionDao(): OptionDao
|
abstract fun optionDao(): OptionDao
|
||||||
|
abstract fun trustedDownloaderPluginDao(): TrustedDownloaderPluginDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun generateUid() = Random.Default.nextInt()
|
fun generateUid() = Random.Default.nextInt()
|
||||||
|
@ -12,4 +12,5 @@ data class DownloadedApp(
|
|||||||
@ColumnInfo(name = "package_name") val packageName: String,
|
@ColumnInfo(name = "package_name") val packageName: String,
|
||||||
@ColumnInfo(name = "version") val version: String,
|
@ColumnInfo(name = "version") val version: String,
|
||||||
@ColumnInfo(name = "directory") val directory: File,
|
@ColumnInfo(name = "directory") val directory: File,
|
||||||
|
@ColumnInfo(name = "last_used") val lastUsed: Long = System.currentTimeMillis()
|
||||||
)
|
)
|
@ -4,6 +4,7 @@ import androidx.room.Dao
|
|||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import androidx.room.Upsert
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@ -14,8 +15,11 @@ interface DownloadedAppDao {
|
|||||||
@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?
|
||||||
|
|
||||||
@Insert
|
@Upsert
|
||||||
suspend fun insert(downloadedApp: DownloadedApp)
|
suspend fun upsert(downloadedApp: DownloadedApp)
|
||||||
|
|
||||||
|
@Query("UPDATE downloaded_app SET last_used = :newValue WHERE package_name = :packageName AND version = :version")
|
||||||
|
suspend fun markUsed(packageName: String, version: String, newValue: Long = System.currentTimeMillis())
|
||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
suspend fun delete(downloadedApps: Collection<DownloadedApp>)
|
suspend fun delete(downloadedApps: Collection<DownloadedApp>)
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
package app.revanced.manager.data.room.apps.installed
|
package app.revanced.manager.data.room.apps.installed
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
enum class InstallType(val stringResource: Int) {
|
enum class InstallType(val stringResource: Int) {
|
||||||
DEFAULT(R.string.default_install),
|
DEFAULT(R.string.default_install),
|
||||||
MOUNT(R.string.mount_install)
|
MOUNT(R.string.mount_install)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
@Entity(tableName = "installed_app")
|
@Entity(tableName = "installed_app")
|
||||||
data class InstalledApp(
|
data class InstalledApp(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@ -20,4 +17,4 @@ data class InstalledApp(
|
|||||||
@ColumnInfo(name = "original_package_name") val originalPackageName: String,
|
@ColumnInfo(name = "original_package_name") val originalPackageName: String,
|
||||||
@ColumnInfo(name = "version") val version: String,
|
@ColumnInfo(name = "version") val version: String,
|
||||||
@ColumnInfo(name = "install_type") val installType: InstallType
|
@ColumnInfo(name = "install_type") val installType: InstallType
|
||||||
) : Parcelable
|
)
|
@ -8,11 +8,11 @@ interface PatchBundleDao {
|
|||||||
@Query("SELECT * FROM patch_bundles")
|
@Query("SELECT * FROM patch_bundles")
|
||||||
suspend fun all(): List<PatchBundleEntity>
|
suspend fun all(): List<PatchBundleEntity>
|
||||||
|
|
||||||
@Query("SELECT version, integrations_version, auto_update FROM patch_bundles WHERE uid = :uid")
|
@Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid")
|
||||||
fun getPropsById(uid: Int): Flow<BundleProperties?>
|
fun getPropsById(uid: Int): Flow<BundleProperties?>
|
||||||
|
|
||||||
@Query("UPDATE patch_bundles SET version = :patches, integrations_version = :integrations WHERE uid = :uid")
|
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
|
||||||
suspend fun updateVersion(uid: Int, patches: String?, integrations: String?)
|
suspend fun updateVersion(uid: Int, patches: String?)
|
||||||
|
|
||||||
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
|
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
|
||||||
suspend fun setAutoUpdate(uid: Int, value: Boolean)
|
suspend fun setAutoUpdate(uid: Int, value: Boolean)
|
||||||
@ -26,7 +26,7 @@ interface PatchBundleDao {
|
|||||||
@Transaction
|
@Transaction
|
||||||
suspend fun reset() {
|
suspend fun reset() {
|
||||||
purgeCustomBundles()
|
purgeCustomBundles()
|
||||||
updateVersion(0, null, null) // Reset the main source
|
updateVersion(0, null) // Reset the main source
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
|
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
|
||||||
|
@ -29,21 +29,16 @@ sealed class Source {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class VersionInfo(
|
|
||||||
@ColumnInfo(name = "version") val patches: String? = null,
|
|
||||||
@ColumnInfo(name = "integrations_version") val integrations: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Entity(tableName = "patch_bundles")
|
@Entity(tableName = "patch_bundles")
|
||||||
data class PatchBundleEntity(
|
data class PatchBundleEntity(
|
||||||
@PrimaryKey val uid: Int,
|
@PrimaryKey val uid: Int,
|
||||||
@ColumnInfo(name = "name") val name: String,
|
@ColumnInfo(name = "name") val name: String,
|
||||||
@Embedded val versionInfo: VersionInfo,
|
@ColumnInfo(name = "version") val version: String? = null,
|
||||||
@ColumnInfo(name = "source") val source: Source,
|
@ColumnInfo(name = "source") val source: Source,
|
||||||
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BundleProperties(
|
data class BundleProperties(
|
||||||
@Embedded val versionInfo: VersionInfo,
|
@ColumnInfo(name = "version") val version: String? = null,
|
||||||
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||||
)
|
)
|
@ -20,6 +20,8 @@ import kotlinx.serialization.json.jsonArray
|
|||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import kotlinx.serialization.json.long
|
import kotlinx.serialization.json.long
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KType
|
||||||
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "options",
|
tableName = "options",
|
||||||
@ -46,8 +48,8 @@ data class Option(
|
|||||||
|
|
||||||
val errorMessage = "Cannot deserialize value as ${option.type}"
|
val errorMessage = "Cannot deserialize value as ${option.type}"
|
||||||
try {
|
try {
|
||||||
if (option.type.endsWith("Array")) {
|
if (option.type.classifier == List::class) {
|
||||||
val elementType = option.type.removeSuffix("Array")
|
val elementType = option.type.arguments.first().type!!
|
||||||
return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) }
|
return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,12 +69,17 @@ data class Option(
|
|||||||
allowSpecialFloatingPointValues = true
|
allowSpecialFloatingPointValues = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deserializeBasicType(type: String, value: JsonPrimitive) = when (type) {
|
private fun deserializeBasicType(type: KType, value: JsonPrimitive) = when (type) {
|
||||||
"Boolean" -> value.boolean
|
typeOf<Boolean>() -> value.boolean
|
||||||
"Int" -> value.int
|
typeOf<Int>() -> value.int
|
||||||
"Long" -> value.long
|
typeOf<Long>() -> value.long
|
||||||
"Float" -> value.float
|
typeOf<Float>() -> value.float
|
||||||
"String" -> value.content.also { if (!value.isString) throw SerializationException("Expected value to be a string: $value") }
|
typeOf<String>() -> value.content.also {
|
||||||
|
if (!value.isString) throw SerializationException(
|
||||||
|
"Expected value to be a string: $value"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
else -> throw SerializationException("Unknown type: $type")
|
else -> throw SerializationException("Unknown type: $type")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
package app.revanced.manager.data.room.plugins
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "trusted_downloader_plugins")
|
||||||
|
class TrustedDownloaderPlugin(
|
||||||
|
@PrimaryKey @ColumnInfo(name = "package_name") val packageName: String,
|
||||||
|
@ColumnInfo(name = "signature") val signature: ByteArray
|
||||||
|
)
|
@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.manager.data.room.plugins
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Upsert
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface TrustedDownloaderPluginDao {
|
||||||
|
@Query("SELECT signature FROM trusted_downloader_plugins WHERE package_name = :packageName")
|
||||||
|
suspend fun getTrustedSignature(packageName: String): ByteArray?
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
suspend fun upsertTrust(plugin: TrustedDownloaderPlugin)
|
||||||
|
|
||||||
|
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name = :packageName")
|
||||||
|
suspend fun remove(packageName: String)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name IN (:packageNames)")
|
||||||
|
suspend fun removeAll(packageNames: Set<String>)
|
||||||
|
}
|
@ -22,6 +22,7 @@ val repositoryModule = module {
|
|||||||
// It is best to load patch bundles ASAP
|
// It is best to load patch bundles ASAP
|
||||||
createdAtStart()
|
createdAtStart()
|
||||||
}
|
}
|
||||||
|
singleOf(::DownloaderPluginRepository)
|
||||||
singleOf(::WorkerRepository)
|
singleOf(::WorkerRepository)
|
||||||
singleOf(::DownloadedAppRepository)
|
singleOf(::DownloadedAppRepository)
|
||||||
singleOf(::InstalledAppRepository)
|
singleOf(::InstalledAppRepository)
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
package app.revanced.manager.di
|
package app.revanced.manager.di
|
||||||
|
|
||||||
import app.revanced.manager.network.service.HttpService
|
import app.revanced.manager.network.service.HttpService
|
||||||
import app.revanced.manager.network.service.ReVancedService
|
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val serviceModule = module {
|
val serviceModule = module {
|
||||||
singleOf(::ReVancedService)
|
|
||||||
singleOf(::HttpService)
|
singleOf(::HttpService)
|
||||||
}
|
}
|
@ -9,10 +9,9 @@ val viewModelModule = module {
|
|||||||
viewModelOf(::DashboardViewModel)
|
viewModelOf(::DashboardViewModel)
|
||||||
viewModelOf(::SelectedAppInfoViewModel)
|
viewModelOf(::SelectedAppInfoViewModel)
|
||||||
viewModelOf(::PatchesSelectorViewModel)
|
viewModelOf(::PatchesSelectorViewModel)
|
||||||
viewModelOf(::SettingsViewModel)
|
viewModelOf(::GeneralSettingsViewModel)
|
||||||
viewModelOf(::AdvancedSettingsViewModel)
|
viewModelOf(::AdvancedSettingsViewModel)
|
||||||
viewModelOf(::AppSelectorViewModel)
|
viewModelOf(::AppSelectorViewModel)
|
||||||
viewModelOf(::VersionSelectorViewModel)
|
|
||||||
viewModelOf(::PatcherViewModel)
|
viewModelOf(::PatcherViewModel)
|
||||||
viewModelOf(::UpdateViewModel)
|
viewModelOf(::UpdateViewModel)
|
||||||
viewModelOf(::ChangelogsViewModel)
|
viewModelOf(::ChangelogsViewModel)
|
||||||
|
@ -4,29 +4,18 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.StandardCopyOption
|
|
||||||
|
|
||||||
class LocalPatchBundle(name: String, id: Int, directory: File) :
|
class LocalPatchBundle(name: String, id: Int, directory: File) :
|
||||||
PatchBundleSource(name, id, directory) {
|
PatchBundleSource(name, id, directory) {
|
||||||
suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) {
|
suspend fun replace(patches: InputStream) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
patches?.let { inputStream ->
|
patchBundleOutputStream().use { outputStream ->
|
||||||
patchBundleOutputStream().use { outputStream ->
|
patches.copyTo(outputStream)
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
integrations?.let {
|
|
||||||
Files.copy(
|
|
||||||
it,
|
|
||||||
this@LocalPatchBundle.integrationsFile.toPath(),
|
|
||||||
StandardCopyOption.REPLACE_EXISTING
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reload()?.also {
|
reload()?.also {
|
||||||
saveVersion(it.readManifestAttribute("Version"), null)
|
saveVersion(it.readManifestAttribute("Version"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,6 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
|
|||||||
protected val configRepository: PatchBundlePersistenceRepository by inject()
|
protected val configRepository: PatchBundlePersistenceRepository by inject()
|
||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
protected val patchesFile = directory.resolve("patches.jar")
|
protected val patchesFile = directory.resolve("patches.jar")
|
||||||
protected val integrationsFile = directory.resolve("integrations.apk")
|
|
||||||
|
|
||||||
private val _state = MutableStateFlow(load())
|
private val _state = MutableStateFlow(load())
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
@ -58,7 +57,7 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
|
|||||||
if (!hasInstalled()) return State.Missing
|
if (!hasInstalled()) return State.Missing
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
|
State.Loaded(PatchBundle(patchesFile))
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(tag, "Failed to load patch bundle with UID $uid", t)
|
Log.e(tag, "Failed to load patch bundle with UID $uid", t)
|
||||||
State.Failed(t)
|
State.Failed(t)
|
||||||
@ -85,9 +84,9 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
|
|||||||
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
|
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
|
||||||
suspend fun getProps() = propsFlow().first()!!
|
suspend fun getProps() = propsFlow().first()!!
|
||||||
|
|
||||||
suspend fun currentVersion() = getProps().versionInfo
|
suspend fun currentVersion() = getProps().version
|
||||||
protected suspend fun saveVersion(patches: String?, integrations: String?) =
|
protected suspend fun saveVersion(version: String?) =
|
||||||
configRepository.updateVersion(uid, patches, integrations)
|
configRepository.updateVersion(uid, version)
|
||||||
|
|
||||||
suspend fun setName(name: String) {
|
suspend fun setName(name: String) {
|
||||||
configRepository.setName(uid, name)
|
configRepository.setName(uid, name)
|
||||||
|
@ -1,20 +1,12 @@
|
|||||||
package app.revanced.manager.domain.bundles
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import app.revanced.manager.data.room.bundles.VersionInfo
|
|
||||||
import app.revanced.manager.network.api.ReVancedAPI
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
import app.revanced.manager.network.dto.BundleAsset
|
|
||||||
import app.revanced.manager.network.dto.BundleInfo
|
|
||||||
import app.revanced.manager.network.service.HttpService
|
import app.revanced.manager.network.service.HttpService
|
||||||
import app.revanced.manager.network.utils.getOrThrow
|
import app.revanced.manager.network.utils.getOrThrow
|
||||||
import app.revanced.manager.util.APK_MIMETYPE
|
|
||||||
import app.revanced.manager.util.JAR_MIMETYPE
|
|
||||||
import io.ktor.client.request.url
|
import io.ktor.client.request.url
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -24,27 +16,16 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
|
|||||||
PatchBundleSource(name, id, directory) {
|
PatchBundleSource(name, id, directory) {
|
||||||
protected val http: HttpService by inject()
|
protected val http: HttpService by inject()
|
||||||
|
|
||||||
protected abstract suspend fun getLatestInfo(): BundleInfo
|
protected abstract suspend fun getLatestInfo(): ReVancedAsset
|
||||||
|
|
||||||
private suspend fun download(info: BundleInfo) = withContext(Dispatchers.IO) {
|
private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
|
||||||
val (patches, integrations) = info
|
patchBundleOutputStream().use {
|
||||||
coroutineScope {
|
http.streamTo(it) {
|
||||||
launch {
|
url(info.downloadUrl)
|
||||||
patchBundleOutputStream().use {
|
|
||||||
http.streamTo(it) {
|
|
||||||
url(patches.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
launch {
|
|
||||||
http.download(integrationsFile) {
|
|
||||||
url(integrations.url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveVersion(patches.version, integrations.version)
|
saveVersion(info.version)
|
||||||
reload()
|
reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,20 +35,15 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
|
|||||||
|
|
||||||
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
|
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
|
||||||
val info = getLatestInfo()
|
val info = getLatestInfo()
|
||||||
if (hasInstalled() && VersionInfo(
|
if (hasInstalled() && info.version == currentVersion())
|
||||||
info.patches.version,
|
|
||||||
info.integrations.version
|
|
||||||
) == currentVersion()
|
|
||||||
) {
|
|
||||||
return@withContext false
|
return@withContext false
|
||||||
}
|
|
||||||
|
|
||||||
download(info)
|
download(info)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
|
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
|
||||||
arrayOf(patchesFile, integrationsFile).forEach(File::delete)
|
patchesFile.delete()
|
||||||
reload()
|
reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +57,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
|
|||||||
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
||||||
RemotePatchBundle(name, id, directory, endpoint) {
|
RemotePatchBundle(name, id, directory, endpoint) {
|
||||||
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
|
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
|
||||||
http.request<BundleInfo> {
|
http.request<ReVancedAsset> {
|
||||||
url(endpoint)
|
url(endpoint)
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
}
|
}
|
||||||
@ -91,22 +67,5 @@ class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
|||||||
RemotePatchBundle(name, id, directory, endpoint) {
|
RemotePatchBundle(name, id, directory, endpoint) {
|
||||||
private val api: ReVancedAPI by inject()
|
private val api: ReVancedAPI by inject()
|
||||||
|
|
||||||
override suspend fun getLatestInfo() = coroutineScope {
|
override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow()
|
||||||
fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) {
|
|
||||||
api
|
|
||||||
.getLatestRelease(repo)
|
|
||||||
.getOrThrow()
|
|
||||||
.let {
|
|
||||||
BundleAsset(it.version, it.findAssetByType(mime).downloadUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val patches = getAssetAsync("revanced-patches", JAR_MIMETYPE)
|
|
||||||
val integrations = getAssetAsync("revanced-integrations", APK_MIMETYPE)
|
|
||||||
|
|
||||||
BundleInfo(
|
|
||||||
patches.await(),
|
|
||||||
integrations.await()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -109,7 +109,12 @@ class RootInstaller(
|
|||||||
|
|
||||||
stockAPK?.let { stockApp ->
|
stockAPK?.let { stockApp ->
|
||||||
pm.getPackageInfo(packageName)?.let { packageInfo ->
|
pm.getPackageInfo(packageName)?.let { packageInfo ->
|
||||||
if (packageInfo.versionName <= version)
|
// TODO: get user id programmatically
|
||||||
|
if (pm.getVersionCode(packageInfo) <= pm.getVersionCode(
|
||||||
|
pm.getPackageInfo(patchedAPK)
|
||||||
|
?: error("Failed to get package info for patched app")
|
||||||
|
)
|
||||||
|
)
|
||||||
execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
|
execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ import java.io.InputStream
|
|||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.security.UnrecoverableKeyException
|
import java.security.UnrecoverableKeyException
|
||||||
|
import java.util.Date
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
||||||
companion object Constants {
|
companion object Constants {
|
||||||
@ -19,6 +21,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
|||||||
* Default alias and password for the keystore.
|
* Default alias and password for the keystore.
|
||||||
*/
|
*/
|
||||||
const val DEFAULT = "ReVanced"
|
const val DEFAULT = "ReVanced"
|
||||||
|
private val eightYearsFromNow get() = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds * 24)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val keystorePath =
|
private val keystorePath =
|
||||||
@ -29,23 +32,26 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
|||||||
prefs.keystorePass.value = pass
|
prefs.keystorePass.value = pass
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun signingOptions(path: File = keystorePath) = ApkUtils.SigningOptions(
|
private suspend fun signingDetails(path: File = keystorePath) = ApkUtils.KeyStoreDetails(
|
||||||
keyStore = path,
|
keyStore = path,
|
||||||
keyStorePassword = null,
|
keyStorePassword = null,
|
||||||
alias = prefs.keystoreCommonName.get(),
|
alias = prefs.keystoreCommonName.get(),
|
||||||
signer = prefs.keystoreCommonName.get(),
|
|
||||||
password = prefs.keystorePass.get()
|
password = prefs.keystorePass.get()
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
|
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
|
||||||
ApkUtils.sign(input, output, signingOptions())
|
ApkUtils.signApk(input, output, prefs.keystoreCommonName.get(), signingDetails())
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun regenerate() = withContext(Dispatchers.Default) {
|
suspend fun regenerate() = withContext(Dispatchers.Default) {
|
||||||
|
val keyCertPair = ApkSigner.newPrivateKeyCertificatePair(
|
||||||
|
prefs.keystoreCommonName.get(),
|
||||||
|
eightYearsFromNow
|
||||||
|
)
|
||||||
val ks = ApkSigner.newKeyStore(
|
val ks = ApkSigner.newKeyStore(
|
||||||
setOf(
|
setOf(
|
||||||
ApkSigner.KeyStoreEntry(
|
ApkSigner.KeyStoreEntry(
|
||||||
DEFAULT, DEFAULT
|
DEFAULT, DEFAULT, keyCertPair
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -64,7 +70,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
|||||||
try {
|
try {
|
||||||
val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
|
val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
|
||||||
|
|
||||||
ApkSigner.readKeyCertificatePair(ks, cn, pass)
|
ApkSigner.readPrivateKeyCertificatePair(ks, cn, pass)
|
||||||
} catch (_: UnrecoverableKeyException) {
|
} catch (_: UnrecoverableKeyException) {
|
||||||
return false
|
return false
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
|
@ -12,15 +12,12 @@ class PreferencesManager(
|
|||||||
|
|
||||||
val api = stringPreference("api_url", "https://api.revanced.app")
|
val api = stringPreference("api_url", "https://api.revanced.app")
|
||||||
|
|
||||||
val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true)
|
|
||||||
val useProcessRuntime = booleanPreference("use_process_runtime", false)
|
val useProcessRuntime = booleanPreference("use_process_runtime", false)
|
||||||
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)
|
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)
|
||||||
|
|
||||||
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
||||||
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
||||||
|
|
||||||
val preferSplits = booleanPreference("prefer_splits", false)
|
|
||||||
|
|
||||||
val firstLaunch = booleanPreference("first_launch", true)
|
val firstLaunch = booleanPreference("first_launch", true)
|
||||||
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
||||||
val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true)
|
val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true)
|
||||||
@ -29,4 +26,6 @@ class PreferencesManager(
|
|||||||
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
||||||
val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false)
|
val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false)
|
||||||
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
||||||
|
|
||||||
|
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,9 @@ abstract class BasePreferencesManager(private val context: Context, name: String
|
|||||||
protected fun stringPreference(key: String, default: String) =
|
protected fun stringPreference(key: String, default: String) =
|
||||||
StringPreference(dataStore, key, default)
|
StringPreference(dataStore, key, default)
|
||||||
|
|
||||||
|
protected fun stringSetPreference(key: String, default: Set<String>) =
|
||||||
|
StringSetPreference(dataStore, key, default)
|
||||||
|
|
||||||
protected fun booleanPreference(key: String, default: Boolean) =
|
protected fun booleanPreference(key: String, default: Boolean) =
|
||||||
BooleanPreference(dataStore, key, default)
|
BooleanPreference(dataStore, key, default)
|
||||||
|
|
||||||
@ -52,6 +55,10 @@ class EditorContext(private val prefs: MutablePreferences) {
|
|||||||
var <T> Preference<T>.value
|
var <T> Preference<T>.value
|
||||||
get() = prefs.run { read() }
|
get() = prefs.run { read() }
|
||||||
set(value) = prefs.run { write(value) }
|
set(value) = prefs.run { write(value) }
|
||||||
|
|
||||||
|
operator fun Preference<Set<String>>.plusAssign(value: String) = prefs.run {
|
||||||
|
write(read() + value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class Preference<T>(
|
abstract class Preference<T>(
|
||||||
@ -65,10 +72,12 @@ abstract class Preference<T>(
|
|||||||
|
|
||||||
suspend fun get() = flow.first()
|
suspend fun get() = flow.first()
|
||||||
fun getBlocking() = runBlocking { get() }
|
fun getBlocking() = runBlocking { get() }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember {
|
fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember {
|
||||||
getBlocking()
|
getBlocking()
|
||||||
})
|
})
|
||||||
|
|
||||||
suspend fun update(value: T) = dataStore.editor {
|
suspend fun update(value: T) = dataStore.editor {
|
||||||
this@Preference.value = value
|
this@Preference.value = value
|
||||||
}
|
}
|
||||||
@ -108,6 +117,14 @@ class StringPreference(
|
|||||||
override val key = stringPreferencesKey(key)
|
override val key = stringPreferencesKey(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StringSetPreference(
|
||||||
|
dataStore: DataStore<Preferences>,
|
||||||
|
key: String,
|
||||||
|
default: Set<String>
|
||||||
|
) : BasePreference<Set<String>>(dataStore, default) {
|
||||||
|
override val key = stringSetPreferencesKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
class BooleanPreference(
|
class BooleanPreference(
|
||||||
dataStore: DataStore<Preferences>,
|
dataStore: DataStore<Preferences>,
|
||||||
key: String,
|
key: String,
|
||||||
|
@ -2,56 +2,126 @@ package app.revanced.manager.domain.repository
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Parcelable
|
||||||
import app.revanced.manager.data.room.AppDatabase
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||||
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
||||||
import app.revanced.manager.network.downloader.AppDownloader
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
|
import app.revanced.manager.plugin.downloader.OutputDownloadScope
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.flow.conflate
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FilterOutputStream
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
|
|
||||||
class DownloadedAppRepository(
|
class DownloadedAppRepository(
|
||||||
app: Application,
|
private val app: Application,
|
||||||
db: AppDatabase
|
db: AppDatabase,
|
||||||
|
private val pm: PM
|
||||||
) {
|
) {
|
||||||
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
|
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
|
||||||
private val dao = db.downloadedAppDao()
|
private val dao = db.downloadedAppDao()
|
||||||
|
|
||||||
fun getAll() = dao.getAllApps().distinctUntilChanged()
|
fun getAll() = dao.getAllApps().distinctUntilChanged()
|
||||||
|
|
||||||
fun getApkFileForApp(app: DownloadedApp): File = getApkFileForDir(dir.resolve(app.directory))
|
fun getApkFileForApp(app: DownloadedApp): File =
|
||||||
|
getApkFileForDir(dir.resolve(app.directory))
|
||||||
|
|
||||||
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
|
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
|
||||||
|
|
||||||
suspend fun download(
|
suspend fun download(
|
||||||
app: AppDownloader.App,
|
plugin: LoadedDownloaderPlugin,
|
||||||
preferSplits: Boolean,
|
data: Parcelable,
|
||||||
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit = {},
|
expectedPackageName: String,
|
||||||
|
expectedVersion: String?,
|
||||||
|
onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit,
|
||||||
): File {
|
): File {
|
||||||
this.get(app.packageName, app.version)?.let { downloaded ->
|
|
||||||
return getApkFileForApp(downloaded)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
||||||
val relativePath = File(generateUid().toString())
|
val relativePath = File(generateUid().toString())
|
||||||
val savePath = dir.resolve(relativePath).also { it.mkdirs() }
|
val saveDir = dir.resolve(relativePath).also { it.mkdirs() }
|
||||||
|
val targetFile = saveDir.resolve("base.apk").toPath()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
app.download(savePath, preferSplits, onDownload)
|
val downloadSize = AtomicLong(0)
|
||||||
|
val downloadedBytes = AtomicLong(0)
|
||||||
|
|
||||||
dao.insert(DownloadedApp(
|
channelFlow {
|
||||||
packageName = app.packageName,
|
val scope = object : OutputDownloadScope {
|
||||||
version = app.version,
|
override val pluginPackageName = plugin.packageName
|
||||||
directory = relativePath,
|
override val hostPackageName = app.packageName
|
||||||
))
|
override suspend fun reportSize(size: Long) {
|
||||||
|
require(size > 0) { "Size must be greater than zero" }
|
||||||
|
require(
|
||||||
|
downloadSize.compareAndSet(
|
||||||
|
0,
|
||||||
|
size
|
||||||
|
)
|
||||||
|
) { "Download size has already been set" }
|
||||||
|
send(downloadedBytes.get() to size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitProgress(bytes: Long) {
|
||||||
|
val newValue = downloadedBytes.addAndGet(bytes)
|
||||||
|
val totalSize = downloadSize.get()
|
||||||
|
if (totalSize < 1) return
|
||||||
|
trySend(newValue to totalSize).getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
targetFile.outputStream(StandardOpenOption.CREATE_NEW).buffered().use {
|
||||||
|
val stream = object : FilterOutputStream(it) {
|
||||||
|
override fun write(b: Int) = out.write(b).also { emitProgress(1) }
|
||||||
|
|
||||||
|
override fun write(b: ByteArray?, off: Int, len: Int) =
|
||||||
|
out.write(b, off, len).also {
|
||||||
|
emitProgress(
|
||||||
|
(len - off).toLong()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.download(scope, data, stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.conflate()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { (downloaded, size) -> onDownload(downloaded to size) }
|
||||||
|
|
||||||
|
if (downloadedBytes.get() < 1) error("Downloader did not download anything.")
|
||||||
|
val pkgInfo =
|
||||||
|
pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid")
|
||||||
|
if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}")
|
||||||
|
if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}")
|
||||||
|
|
||||||
|
// Delete the previous copy (if present).
|
||||||
|
dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let {
|
||||||
|
if (!dir.resolve(it).deleteRecursively()) throw Exception("Failed to delete existing directory")
|
||||||
|
}
|
||||||
|
dao.upsert(
|
||||||
|
DownloadedApp(
|
||||||
|
packageName = pkgInfo.packageName,
|
||||||
|
version = pkgInfo.versionName!!,
|
||||||
|
directory = relativePath,
|
||||||
|
)
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
savePath.deleteRecursively()
|
saveDir.deleteRecursively()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the Apk file.
|
// Return the Apk file.
|
||||||
return getApkFileForDir(savePath)
|
return getApkFileForDir(saveDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun get(packageName: String, version: String) = dao.get(packageName, version)
|
suspend fun get(packageName: String, version: String, markUsed: Boolean = false) =
|
||||||
|
dao.get(packageName, version)?.also {
|
||||||
|
if (markUsed) dao.markUsed(packageName, version)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun delete(downloadedApps: Collection<DownloadedApp>) {
|
suspend fun delete(downloadedApps: Collection<DownloadedApp>) {
|
||||||
downloadedApps.forEach {
|
downloadedApps.forEach {
|
||||||
|
@ -0,0 +1,168 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||||
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
|
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||||
|
import app.revanced.manager.plugin.downloader.DownloaderBuilder
|
||||||
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
|
import app.revanced.manager.plugin.downloader.Scope
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import dalvik.system.PathClassLoader
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
|
||||||
|
@OptIn(PluginHostApi::class)
|
||||||
|
class DownloaderPluginRepository(
|
||||||
|
private val pm: PM,
|
||||||
|
private val prefs: PreferencesManager,
|
||||||
|
private val app: Application,
|
||||||
|
db: AppDatabase
|
||||||
|
) {
|
||||||
|
private val trustDao = db.trustedDownloaderPluginDao()
|
||||||
|
private val _pluginStates = MutableStateFlow(emptyMap<String, DownloaderPluginState>())
|
||||||
|
val pluginStates = _pluginStates.asStateFlow()
|
||||||
|
val loadedPluginsFlow = pluginStates.map { states ->
|
||||||
|
states.values.filterIsInstance<DownloaderPluginState.Loaded>().map { it.plugin }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val acknowledgedDownloaderPlugins = prefs.acknowledgedDownloaderPlugins
|
||||||
|
private val installedPluginPackageNames = MutableStateFlow(emptySet<String>())
|
||||||
|
val newPluginPackageNames = combine(
|
||||||
|
installedPluginPackageNames,
|
||||||
|
acknowledgedDownloaderPlugins.flow
|
||||||
|
) { installed, acknowledged ->
|
||||||
|
installed subtract acknowledged
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reload() {
|
||||||
|
val plugins =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
pm.getPackagesWithFeature(PLUGIN_FEATURE)
|
||||||
|
.associate { it.packageName to loadPlugin(it.packageName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
_pluginStates.value = plugins
|
||||||
|
installedPluginPackageNames.value = plugins.keys
|
||||||
|
|
||||||
|
val acknowledgedPlugins = acknowledgedDownloaderPlugins.get()
|
||||||
|
val uninstalledPlugins = acknowledgedPlugins subtract installedPluginPackageNames.value
|
||||||
|
if (uninstalledPlugins.isNotEmpty()) {
|
||||||
|
Log.d(tag, "Uninstalled plugins: ${uninstalledPlugins.joinToString(", ")}")
|
||||||
|
acknowledgedDownloaderPlugins.update(acknowledgedPlugins subtract uninstalledPlugins)
|
||||||
|
trustDao.removeAll(uninstalledPlugins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unwrapParceledData(data: ParceledDownloaderData): Pair<LoadedDownloaderPlugin, Parcelable> {
|
||||||
|
val plugin =
|
||||||
|
(_pluginStates.value[data.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin
|
||||||
|
?: throw Exception("Downloader plugin with name ${data.pluginPackageName} is not available")
|
||||||
|
|
||||||
|
return plugin to data.unwrapWith(plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadPlugin(packageName: String): DownloaderPluginState {
|
||||||
|
try {
|
||||||
|
if (!verify(packageName)) return DownloaderPluginState.Untrusted
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(tag, "Got exception while verifying plugin $packageName", e)
|
||||||
|
return DownloaderPluginState.Failed(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!!
|
||||||
|
val className = packageInfo.applicationInfo!!.metaData.getString(METADATA_PLUGIN_CLASS)
|
||||||
|
?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS")
|
||||||
|
|
||||||
|
val classLoader =
|
||||||
|
PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader)
|
||||||
|
val pluginContext = app.createPackageContext(packageName, 0)
|
||||||
|
|
||||||
|
val downloader = classLoader
|
||||||
|
.loadClass(className)
|
||||||
|
.getDownloaderBuilder()
|
||||||
|
.build(
|
||||||
|
scopeImpl = object : Scope {
|
||||||
|
override val hostPackageName = app.packageName
|
||||||
|
override val pluginPackageName = pluginContext.packageName
|
||||||
|
},
|
||||||
|
context = pluginContext
|
||||||
|
)
|
||||||
|
|
||||||
|
DownloaderPluginState.Loaded(
|
||||||
|
LoadedDownloaderPlugin(
|
||||||
|
packageName,
|
||||||
|
with(pm) { packageInfo.label() },
|
||||||
|
packageInfo.versionName!!,
|
||||||
|
downloader.get,
|
||||||
|
downloader.download,
|
||||||
|
classLoader
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.e(tag, "Failed to load plugin $packageName", t)
|
||||||
|
DownloaderPluginState.Failed(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun trustPackage(packageName: String) {
|
||||||
|
trustDao.upsertTrust(
|
||||||
|
TrustedDownloaderPlugin(
|
||||||
|
packageName,
|
||||||
|
pm.getSignature(packageName).toByteArray()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
reload()
|
||||||
|
prefs.edit {
|
||||||
|
acknowledgedDownloaderPlugins += packageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun revokeTrustForPackage(packageName: String) =
|
||||||
|
trustDao.remove(packageName).also { reload() }
|
||||||
|
|
||||||
|
suspend fun acknowledgeAllNewPlugins() =
|
||||||
|
acknowledgedDownloaderPlugins.update(installedPluginPackageNames.value)
|
||||||
|
|
||||||
|
private suspend fun verify(packageName: String): Boolean {
|
||||||
|
val expectedSignature =
|
||||||
|
trustDao.getTrustedSignature(packageName) ?: return false
|
||||||
|
|
||||||
|
return pm.hasSignature(packageName, expectedSignature)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val PLUGIN_FEATURE = "app.revanced.manager.plugin.downloader"
|
||||||
|
const val METADATA_PLUGIN_CLASS = "app.revanced.manager.plugin.downloader.class"
|
||||||
|
|
||||||
|
const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
|
||||||
|
val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
|
||||||
|
val Class<*>.isDownloaderBuilder get() = DownloaderBuilder::class.java.isAssignableFrom(this)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun Class<*>.getDownloaderBuilder() =
|
||||||
|
declaredMethods
|
||||||
|
.firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() }
|
||||||
|
?.let { it(null) as DownloaderBuilder<Parcelable> }
|
||||||
|
?: throw Exception("Could not find a valid downloader implementation in class $canonicalName")
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,6 @@ import app.revanced.manager.data.room.AppDatabase
|
|||||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
import app.revanced.manager.data.room.bundles.Source
|
import app.revanced.manager.data.room.bundles.Source
|
||||||
import app.revanced.manager.data.room.bundles.VersionInfo
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
class PatchBundlePersistenceRepository(db: AppDatabase) {
|
class PatchBundlePersistenceRepository(db: AppDatabase) {
|
||||||
@ -26,7 +25,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
|
|||||||
PatchBundleEntity(
|
PatchBundleEntity(
|
||||||
uid = generateUid(),
|
uid = generateUid(),
|
||||||
name = name,
|
name = name,
|
||||||
versionInfo = VersionInfo(),
|
version = null,
|
||||||
source = source,
|
source = source,
|
||||||
autoUpdate = autoUpdate
|
autoUpdate = autoUpdate
|
||||||
).also {
|
).also {
|
||||||
@ -35,8 +34,8 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
|
|||||||
|
|
||||||
suspend fun delete(uid: Int) = dao.remove(uid)
|
suspend fun delete(uid: Int) = dao.remove(uid)
|
||||||
|
|
||||||
suspend fun updateVersion(uid: Int, patches: String?, integrations: String?) =
|
suspend fun updateVersion(uid: Int, version: String?) =
|
||||||
dao.updateVersion(uid, patches, integrations)
|
dao.updateVersion(uid, version)
|
||||||
|
|
||||||
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
|
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
|
||||||
|
|
||||||
@ -48,7 +47,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
|
|||||||
val defaultSource = PatchBundleEntity(
|
val defaultSource = PatchBundleEntity(
|
||||||
uid = 0,
|
uid = 0,
|
||||||
name = "",
|
name = "",
|
||||||
versionInfo = VersionInfo(),
|
version = null,
|
||||||
source = Source.API,
|
source = Source.API,
|
||||||
autoUpdate = false
|
autoUpdate = false
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,7 @@ package app.revanced.manager.domain.repository
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import app.revanced.library.PatchUtils
|
import app.revanced.library.mostCommonCompatibleVersions
|
||||||
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.bundles.PatchBundleEntity
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
@ -55,7 +55,7 @@ class PatchBundleRepository(
|
|||||||
val allPatches =
|
val allPatches =
|
||||||
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
||||||
|
|
||||||
PatchUtils.getMostCommonCompatibleVersions(allPatches, countUnusedPatches = true)
|
allPatches.mostCommonCompatibleVersions(countUnusedPatches = true)
|
||||||
.mapValues { (_, versions) ->
|
.mapValues { (_, versions) ->
|
||||||
if (versions.keys.size < 2)
|
if (versions.keys.size < 2)
|
||||||
return@mapValues versions.keys.firstOrNull()
|
return@mapValues versions.keys.firstOrNull()
|
||||||
@ -137,11 +137,11 @@ class PatchBundleRepository(
|
|||||||
private fun addBundle(patchBundle: PatchBundleSource) =
|
private fun addBundle(patchBundle: PatchBundleSource) =
|
||||||
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
|
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
|
||||||
|
|
||||||
suspend fun createLocal(patches: InputStream, integrations: InputStream?) = withContext(Dispatchers.Default) {
|
suspend fun createLocal(patches: InputStream) = withContext(Dispatchers.Default) {
|
||||||
val uid = persistenceRepo.create("", SourceInfo.Local).uid
|
val uid = persistenceRepo.create("", SourceInfo.Local).uid
|
||||||
val bundle = LocalPatchBundle("", uid, directoryOf(uid))
|
val bundle = LocalPatchBundle("", uid, directoryOf(uid))
|
||||||
|
|
||||||
bundle.replace(patches, integrations)
|
bundle.replace(patches)
|
||||||
addBundle(bundle)
|
addBundle(bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,37 +2,41 @@ package app.revanced.manager.network.api
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.network.dto.ReVancedRelease
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
import app.revanced.manager.network.service.ReVancedService
|
import app.revanced.manager.network.dto.ReVancedGitRepository
|
||||||
|
import app.revanced.manager.network.dto.ReVancedInfo
|
||||||
|
import app.revanced.manager.network.service.HttpService
|
||||||
|
import app.revanced.manager.network.utils.APIResponse
|
||||||
import app.revanced.manager.network.utils.getOrThrow
|
import app.revanced.manager.network.utils.getOrThrow
|
||||||
import app.revanced.manager.network.utils.transform
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import io.ktor.client.request.url
|
||||||
|
|
||||||
class ReVancedAPI(
|
class ReVancedAPI(
|
||||||
private val service: ReVancedService,
|
private val client: HttpService,
|
||||||
private val prefs: PreferencesManager
|
private val prefs: PreferencesManager
|
||||||
) {
|
) {
|
||||||
private suspend fun apiUrl() = prefs.api.get()
|
private suspend fun apiUrl() = prefs.api.get()
|
||||||
|
|
||||||
suspend fun getContributors() = service.getContributors(apiUrl()).transform { it.repositories }
|
private suspend inline fun <reified T> request(api: String, route: String): APIResponse<T> =
|
||||||
|
withContext(
|
||||||
|
Dispatchers.IO
|
||||||
|
) {
|
||||||
|
client.request {
|
||||||
|
url("$api/v4/$route")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getLatestRelease(name: String) =
|
private suspend inline fun <reified T> request(route: String) = request<T>(apiUrl(), route)
|
||||||
service.getLatestRelease(apiUrl(), name).transform { it.release }
|
|
||||||
|
|
||||||
suspend fun getReleases(name: String) =
|
|
||||||
service.getReleases(apiUrl(), name).transform { it.releases }
|
|
||||||
|
|
||||||
suspend fun getAppUpdate() =
|
suspend fun getAppUpdate() =
|
||||||
getLatestRelease("revanced-manager")
|
getLatestAppInfo().getOrThrow().takeIf { it.version != Build.VERSION.RELEASE }
|
||||||
.getOrThrow()
|
|
||||||
.takeIf { it.version != Build.VERSION.RELEASE }
|
|
||||||
|
|
||||||
suspend fun getInfo(api: String? = null) = service.getInfo(api ?: apiUrl()).transform { it.info }
|
suspend fun getLatestAppInfo() = request<ReVancedAsset>("manager")
|
||||||
|
|
||||||
|
suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches")
|
||||||
|
|
||||||
companion object Extensions {
|
suspend fun getContributors() = request<List<ReVancedGitRepository>>("contributors")
|
||||||
fun ReVancedRelease.findAssetByType(mime: String) =
|
|
||||||
assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime)
|
suspend fun getInfo(api: String? = null) = request<ReVancedInfo>(api ?: apiUrl(), "about")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MissingAssetException(type: String) : Exception("No asset with type $type")
|
|
@ -1,277 +0,0 @@
|
|||||||
package app.revanced.manager.network.downloader
|
|
||||||
|
|
||||||
import android.os.Build.SUPPORTED_ABIS
|
|
||||||
import app.revanced.manager.network.service.HttpService
|
|
||||||
import io.ktor.client.plugins.onDownload
|
|
||||||
import io.ktor.client.request.parameter
|
|
||||||
import io.ktor.client.request.url
|
|
||||||
import it.skrape.selects.html5.a
|
|
||||||
import it.skrape.selects.html5.div
|
|
||||||
import it.skrape.selects.html5.form
|
|
||||||
import it.skrape.selects.html5.h5
|
|
||||||
import it.skrape.selects.html5.input
|
|
||||||
import it.skrape.selects.html5.p
|
|
||||||
import it.skrape.selects.html5.span
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
import org.koin.core.component.get
|
|
||||||
import org.koin.core.component.inject
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class APKMirror : AppDownloader, KoinComponent {
|
|
||||||
private val httpClient: HttpService = get()
|
|
||||||
|
|
||||||
enum class APKType {
|
|
||||||
APK,
|
|
||||||
BUNDLE
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Variant(
|
|
||||||
val apkType: APKType,
|
|
||||||
val arch: String,
|
|
||||||
val link: String
|
|
||||||
)
|
|
||||||
|
|
||||||
private suspend fun getAppLink(packageName: String): String {
|
|
||||||
val searchResults = httpClient.getHtml { url("$APK_MIRROR/?post_type=app_release&searchtype=app&s=$packageName") }
|
|
||||||
.div {
|
|
||||||
withId = "content"
|
|
||||||
findFirst {
|
|
||||||
div {
|
|
||||||
withClass = "listWidget"
|
|
||||||
findAll {
|
|
||||||
|
|
||||||
find {
|
|
||||||
it.children.first().text.contains(packageName)
|
|
||||||
}!!.children.mapNotNull {
|
|
||||||
if (it.classNames.isEmpty()) {
|
|
||||||
it.h5 {
|
|
||||||
withClass = "appRowTitle"
|
|
||||||
findFirst {
|
|
||||||
a {
|
|
||||||
findFirst {
|
|
||||||
attribute("href")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchResults.find { url ->
|
|
||||||
httpClient.getHtml { url(APK_MIRROR + url) }
|
|
||||||
.div {
|
|
||||||
withId = "primary"
|
|
||||||
findFirst {
|
|
||||||
div {
|
|
||||||
withClass = "tab-buttons"
|
|
||||||
findFirst {
|
|
||||||
div {
|
|
||||||
withClass = "tab-button-positioning"
|
|
||||||
findFirst {
|
|
||||||
children.any {
|
|
||||||
it.attribute("href") == "https://play.google.com/store/apps/details?id=$packageName"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: throw Exception("App isn't available for download")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAvailableVersions(packageName: String, versionFilter: Set<String>) = flow<AppDownloader.App> {
|
|
||||||
|
|
||||||
// We have to hardcode some apps since there are multiple apps with that package name
|
|
||||||
val appCategory = when (packageName) {
|
|
||||||
"com.google.android.apps.youtube.music" -> "youtube-music"
|
|
||||||
"com.google.android.youtube" -> "youtube"
|
|
||||||
else -> getAppLink(packageName).split("/")[3]
|
|
||||||
}
|
|
||||||
|
|
||||||
var page = 1
|
|
||||||
|
|
||||||
val versions = mutableListOf<String>()
|
|
||||||
|
|
||||||
while (
|
|
||||||
if (versionFilter.isNotEmpty())
|
|
||||||
versions.size < versionFilter.size && page <= 7
|
|
||||||
else
|
|
||||||
page <= 1
|
|
||||||
) {
|
|
||||||
httpClient.getHtml {
|
|
||||||
url("$APK_MIRROR/uploads/page/$page/")
|
|
||||||
parameter("appcategory", appCategory)
|
|
||||||
}.div {
|
|
||||||
withClass = "widget_appmanager_recentpostswidget"
|
|
||||||
findFirst {
|
|
||||||
div {
|
|
||||||
withClass = "listWidget"
|
|
||||||
findFirst {
|
|
||||||
children.mapNotNull { element ->
|
|
||||||
if (element.className.isEmpty()) {
|
|
||||||
|
|
||||||
APKMirrorApp(
|
|
||||||
packageName = packageName,
|
|
||||||
version = element.div {
|
|
||||||
withClass = "infoSlide"
|
|
||||||
findFirst {
|
|
||||||
p {
|
|
||||||
findFirst {
|
|
||||||
span {
|
|
||||||
withClass = "infoSlide-value"
|
|
||||||
findFirst {
|
|
||||||
text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
if (it in versionFilter)
|
|
||||||
versions.add(it)
|
|
||||||
},
|
|
||||||
downloadLink = element.findFirst {
|
|
||||||
a {
|
|
||||||
withClass = "downloadLink"
|
|
||||||
findFirst {
|
|
||||||
attribute("href")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onEach { version -> emit(version) }
|
|
||||||
|
|
||||||
page++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
private class APKMirrorApp(
|
|
||||||
override val packageName: String,
|
|
||||||
override val version: String,
|
|
||||||
private val downloadLink: String,
|
|
||||||
) : AppDownloader.App, KoinComponent {
|
|
||||||
@IgnoredOnParcel private val httpClient: HttpService by inject()
|
|
||||||
|
|
||||||
override suspend fun download(
|
|
||||||
saveDirectory: File,
|
|
||||||
preferSplit: Boolean,
|
|
||||||
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit
|
|
||||||
) {
|
|
||||||
val variants = httpClient.getHtml { url(APK_MIRROR + downloadLink) }
|
|
||||||
.div {
|
|
||||||
withClass = "variants-table"
|
|
||||||
findFirst { // list of variants
|
|
||||||
children.drop(1).map {
|
|
||||||
Variant(
|
|
||||||
apkType = it.div {
|
|
||||||
findFirst {
|
|
||||||
span {
|
|
||||||
findFirst {
|
|
||||||
enumValueOf(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
arch = it.div {
|
|
||||||
findSecond {
|
|
||||||
text
|
|
||||||
}
|
|
||||||
},
|
|
||||||
link = it.div {
|
|
||||||
findFirst {
|
|
||||||
a {
|
|
||||||
findFirst {
|
|
||||||
attribute("href")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val orderedAPKTypes = mutableListOf(APKType.APK, APKType.BUNDLE)
|
|
||||||
.also { if (preferSplit) it.reverse() }
|
|
||||||
|
|
||||||
val variant = orderedAPKTypes.firstNotNullOfOrNull { apkType ->
|
|
||||||
supportedArches.firstNotNullOfOrNull { arch ->
|
|
||||||
variants.find { it.arch == arch && it.apkType == apkType }
|
|
||||||
}
|
|
||||||
} ?: throw Exception("No compatible variant found")
|
|
||||||
|
|
||||||
if (variant.apkType == APKType.BUNDLE) throw Exception("Split apks are not supported yet") // TODO
|
|
||||||
|
|
||||||
val downloadPage = httpClient.getHtml { url(APK_MIRROR + variant.link) }
|
|
||||||
.a {
|
|
||||||
withClass = "downloadButton"
|
|
||||||
findFirst {
|
|
||||||
attribute("href")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val downloadLink = httpClient.getHtml { url(APK_MIRROR + downloadPage) }
|
|
||||||
.form {
|
|
||||||
withId = "filedownload"
|
|
||||||
findFirst {
|
|
||||||
val apkLink = attribute("action")
|
|
||||||
val id = input {
|
|
||||||
withAttribute = "name" to "id"
|
|
||||||
findFirst {
|
|
||||||
attribute("value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val key = input {
|
|
||||||
withAttribute = "name" to "key"
|
|
||||||
findFirst {
|
|
||||||
attribute("value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"$apkLink?id=$id&key=$key"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val targetFile = saveDirectory.resolve("base.apk")
|
|
||||||
|
|
||||||
try {
|
|
||||||
httpClient.download(targetFile) {
|
|
||||||
url(APK_MIRROR + downloadLink)
|
|
||||||
onDownload { bytesSentTotal, contentLength ->
|
|
||||||
onDownload(bytesSentTotal.div(100000).toFloat().div(10) to contentLength.div(100000).toFloat().div(10))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variant.apkType == APKType.BUNDLE) {
|
|
||||||
// TODO: Extract temp.zip
|
|
||||||
|
|
||||||
targetFile.delete()
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
onDownload(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val APK_MIRROR = "https://www.apkmirror.com"
|
|
||||||
|
|
||||||
val supportedArches = listOf("universal", "noarch") + SUPPORTED_ABIS
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package app.revanced.manager.network.downloader
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
interface AppDownloader {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all downloadable apps.
|
|
||||||
*
|
|
||||||
* @param packageName The package name of the app.
|
|
||||||
* @param versionFilter A set of versions to filter.
|
|
||||||
*/
|
|
||||||
fun getAvailableVersions(packageName: String, versionFilter: Set<String>): Flow<App>
|
|
||||||
|
|
||||||
interface App : Parcelable {
|
|
||||||
val packageName: String
|
|
||||||
val version: String
|
|
||||||
|
|
||||||
suspend fun download(
|
|
||||||
saveDirectory: File,
|
|
||||||
preferSplit: Boolean,
|
|
||||||
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.manager.network.downloader
|
||||||
|
|
||||||
|
sealed interface DownloaderPluginState {
|
||||||
|
data object Untrusted : DownloaderPluginState
|
||||||
|
|
||||||
|
data class Loaded(val plugin: LoadedDownloaderPlugin) : DownloaderPluginState
|
||||||
|
|
||||||
|
data class Failed(val throwable: Throwable) : DownloaderPluginState
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package app.revanced.manager.network.downloader
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import app.revanced.manager.plugin.downloader.OutputDownloadScope
|
||||||
|
import app.revanced.manager.plugin.downloader.GetScope
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
class LoadedDownloaderPlugin(
|
||||||
|
val packageName: String,
|
||||||
|
val name: String,
|
||||||
|
val version: String,
|
||||||
|
val get: suspend GetScope.(packageName: String, version: String?) -> Pair<Parcelable, String?>?,
|
||||||
|
val download: suspend OutputDownloadScope.(data: Parcelable, outputStream: OutputStream) -> Unit,
|
||||||
|
val classLoader: ClassLoader
|
||||||
|
)
|
@ -0,0 +1,45 @@
|
|||||||
|
package app.revanced.manager.network.downloader
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
/**
|
||||||
|
* A container for [Parcelable] data returned from downloaders. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
|
||||||
|
*/
|
||||||
|
class ParceledDownloaderData private constructor(
|
||||||
|
val pluginPackageName: String,
|
||||||
|
private val bundle: Bundle
|
||||||
|
) : Parcelable {
|
||||||
|
constructor(plugin: LoadedDownloaderPlugin, data: Parcelable) : this(
|
||||||
|
plugin.packageName,
|
||||||
|
createBundle(data)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun unwrapWith(plugin: LoadedDownloaderPlugin): Parcelable {
|
||||||
|
bundle.classLoader = plugin.classLoader
|
||||||
|
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
val className = bundle.getString(CLASS_NAME_KEY)!!
|
||||||
|
val clazz = plugin.classLoader.loadClass(className)
|
||||||
|
|
||||||
|
bundle.getParcelable(DATA_KEY, clazz)!! as Parcelable
|
||||||
|
} else @Suppress("Deprecation") bundle.getParcelable(DATA_KEY)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val CLASS_NAME_KEY = "class"
|
||||||
|
const val DATA_KEY = "data"
|
||||||
|
|
||||||
|
fun createBundle(data: Parcelable) = Bundle().apply {
|
||||||
|
putParcelable(DATA_KEY, data)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) putString(
|
||||||
|
CLASS_NAME_KEY,
|
||||||
|
data::class.java.canonicalName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
package app.revanced.manager.network.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BundleInfo(val patches: BundleAsset, val integrations: BundleAsset)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BundleAsset(val version: String, val url: String)
|
|
@ -1,16 +0,0 @@
|
|||||||
package app.revanced.manager.network.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GithubChangelog(
|
|
||||||
@SerialName("tag_name") val version: String,
|
|
||||||
@SerialName("body") val body: String,
|
|
||||||
@SerialName("assets") val assets: List<GithubAsset>
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GithubAsset(
|
|
||||||
@SerialName("download_count") val downloadCount: Int,
|
|
||||||
)
|
|
@ -0,0 +1,18 @@
|
|||||||
|
package app.revanced.manager.network.dto
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ReVancedAsset (
|
||||||
|
@SerialName("download_url")
|
||||||
|
val downloadUrl: String,
|
||||||
|
@SerialName("created_at")
|
||||||
|
val createdAt: LocalDateTime,
|
||||||
|
@SerialName("signature_download_url")
|
||||||
|
val signatureDownloadUrl: String? = null,
|
||||||
|
val description: String,
|
||||||
|
val version: String,
|
||||||
|
)
|
||||||
|
|
@ -3,19 +3,15 @@ package app.revanced.manager.network.dto
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ReVancedGitRepositories(
|
|
||||||
val repositories: List<ReVancedGitRepository>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ReVancedGitRepository(
|
data class ReVancedGitRepository(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
val url: String,
|
||||||
val contributors: List<ReVancedContributor>,
|
val contributors: List<ReVancedContributor>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ReVancedContributor(
|
data class ReVancedContributor(
|
||||||
@SerialName("login") val username: String,
|
@SerialName("name") val username: String,
|
||||||
@SerialName("avatar_url") val avatarUrl: String,
|
@SerialName("avatar_url") val avatarUrl: String,
|
||||||
)
|
)
|
@ -1,12 +1,8 @@
|
|||||||
package app.revanced.manager.network.dto
|
package app.revanced.manager.network.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ReVancedInfoParent(
|
|
||||||
val info: ReVancedInfo,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ReVancedInfo(
|
data class ReVancedInfo(
|
||||||
val name: String,
|
val name: String,
|
||||||
@ -43,7 +39,8 @@ data class ReVancedDonation(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class ReVancedWallet(
|
data class ReVancedWallet(
|
||||||
val network: String,
|
val network: String,
|
||||||
val currency_code: String,
|
@SerialName("currency_code")
|
||||||
|
val currencyCode: String,
|
||||||
val address: String,
|
val address: String,
|
||||||
val preferred: Boolean
|
val preferred: Boolean
|
||||||
)
|
)
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
package app.revanced.manager.network.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ReVancedLatestRelease(
|
|
||||||
val release: ReVancedRelease,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ReVancedReleases(
|
|
||||||
val releases: List<ReVancedRelease>
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ReVancedRelease(
|
|
||||||
val metadata: ReVancedReleaseMeta,
|
|
||||||
val assets: List<Asset>
|
|
||||||
) {
|
|
||||||
val version get() = metadata.tag
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ReVancedReleaseMeta(
|
|
||||||
@SerialName("tag_name") val tag: String,
|
|
||||||
val name: String,
|
|
||||||
val draft: Boolean,
|
|
||||||
val prerelease: Boolean,
|
|
||||||
@SerialName("created_at") val createdAt: String,
|
|
||||||
@SerialName("published_at") val publishedAt: String,
|
|
||||||
val body: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Asset(
|
|
||||||
val name: String,
|
|
||||||
@SerialName("download_count") val downloadCount: Int,
|
|
||||||
@SerialName("browser_download_url") val downloadUrl: String,
|
|
||||||
@SerialName("content_type") val contentType: String
|
|
||||||
)
|
|
@ -1,43 +0,0 @@
|
|||||||
package app.revanced.manager.network.service
|
|
||||||
|
|
||||||
import app.revanced.manager.network.dto.ReVancedGitRepositories
|
|
||||||
import app.revanced.manager.network.dto.ReVancedInfo
|
|
||||||
import app.revanced.manager.network.dto.ReVancedInfoParent
|
|
||||||
import app.revanced.manager.network.dto.ReVancedLatestRelease
|
|
||||||
import app.revanced.manager.network.dto.ReVancedReleases
|
|
||||||
import app.revanced.manager.network.utils.APIResponse
|
|
||||||
import io.ktor.client.request.url
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class ReVancedService(
|
|
||||||
private val client: HttpService,
|
|
||||||
) {
|
|
||||||
suspend fun getLatestRelease(api: String, repo: String): APIResponse<ReVancedLatestRelease> =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
client.request {
|
|
||||||
url("$api/v2/$repo/releases/latest")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getReleases(api: String, repo: String): APIResponse<ReVancedReleases> =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
client.request {
|
|
||||||
url("$api/v2/$repo/releases")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getContributors(api: String): APIResponse<ReVancedGitRepositories> =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
client.request {
|
|
||||||
url("$api/contributors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getInfo(api: String): APIResponse<ReVancedInfoParent> =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
client.request {
|
|
||||||
url("$api/v2/info")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -22,11 +22,10 @@ class Session(
|
|||||||
cacheDir: String,
|
cacheDir: String,
|
||||||
frameworkDir: String,
|
frameworkDir: String,
|
||||||
aaptPath: String,
|
aaptPath: String,
|
||||||
multithreadingDexFileWriter: Boolean,
|
|
||||||
private val androidContext: Context,
|
private val androidContext: Context,
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
private val input: File,
|
private val input: File,
|
||||||
private val onPatchCompleted: () -> Unit,
|
private val onPatchCompleted: suspend () -> Unit,
|
||||||
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
||||||
) : Closeable {
|
) : Closeable {
|
||||||
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||||
@ -38,8 +37,7 @@ class Session(
|
|||||||
apkFile = input,
|
apkFile = input,
|
||||||
temporaryFilesPath = tempDir,
|
temporaryFilesPath = tempDir,
|
||||||
frameworkFileDirectory = frameworkDir,
|
frameworkFileDirectory = frameworkDir,
|
||||||
aaptBinaryPath = aaptPath,
|
aaptBinaryPath = aaptPath
|
||||||
multithreadingDexFileWriter = multithreadingDexFileWriter,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,7 +49,7 @@ class Session(
|
|||||||
state = State.RUNNING
|
state = State.RUNNING
|
||||||
)
|
)
|
||||||
|
|
||||||
this.apply(true).collect { (patch, exception) ->
|
this().collect { (patch, exception) ->
|
||||||
if (patch !in selectedPatches) return@collect
|
if (patch !in selectedPatches) return@collect
|
||||||
|
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
@ -89,7 +87,7 @@ class Session(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun run(output: File, selectedPatches: PatchList, integrations: List<File>) {
|
suspend fun run(output: File, selectedPatches: PatchList) {
|
||||||
updateProgress(state = State.COMPLETED) // Unpacking
|
updateProgress(state = State.COMPLETED) // Unpacking
|
||||||
|
|
||||||
java.util.logging.Logger.getLogger("").apply {
|
java.util.logging.Logger.getLogger("").apply {
|
||||||
@ -103,8 +101,7 @@ class Session(
|
|||||||
|
|
||||||
with(patcher) {
|
with(patcher) {
|
||||||
logger.info("Merging integrations")
|
logger.info("Merging integrations")
|
||||||
acceptIntegrations(integrations.toSet())
|
this += selectedPatches.toSet()
|
||||||
acceptPatches(selectedPatches.toSet())
|
|
||||||
|
|
||||||
logger.info("Applying patches...")
|
logger.info("Applying patches...")
|
||||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||||
|
@ -2,17 +2,17 @@ package app.revanced.manager.patcher.patch
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
import app.revanced.patcher.PatchBundleLoader
|
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import app.revanced.patcher.patch.PatchLoader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.jar.JarFile
|
import java.util.jar.JarFile
|
||||||
|
|
||||||
class PatchBundle(val patchesJar: File, val integrations: File?) {
|
class PatchBundle(val patchesJar: File) {
|
||||||
private val loader = object : Iterable<Patch<*>> {
|
private val loader = object : Iterable<Patch<*>> {
|
||||||
private fun load(): Iterable<Patch<*>> {
|
private fun load(): Iterable<Patch<*>> {
|
||||||
patchesJar.setReadOnly()
|
patchesJar.setReadOnly()
|
||||||
return PatchBundleLoader.Dex(patchesJar, optimizedDexDirectory = null)
|
return PatchLoader.Dex(setOf(patchesJar))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iterator(): Iterator<Patch<*>> = load().iterator()
|
override fun iterator(): Iterator<Patch<*>> = load().iterator()
|
||||||
@ -41,12 +41,12 @@ class PatchBundle(val patchesJar: File, val integrations: File?) {
|
|||||||
/**
|
/**
|
||||||
* Load all patches compatible with the specified package.
|
* Load all patches compatible with the specified package.
|
||||||
*/
|
*/
|
||||||
fun patchClasses(packageName: String) = loader.filter { patch ->
|
fun patches(packageName: String) = loader.filter { patch ->
|
||||||
val compatiblePackages = patch.compatiblePackages
|
val compatiblePackages = patch.compatiblePackages
|
||||||
?: // The patch has no compatibility constraints, which means it is universal.
|
?: // The patch has no compatibility constraints, which means it is universal.
|
||||||
return@filter true
|
return@filter true
|
||||||
|
|
||||||
if (!compatiblePackages.any { it.name == packageName }) {
|
if (!compatiblePackages.any { (name, _) -> name == packageName }) {
|
||||||
// Patch is not compatible with this package.
|
// Patch is not compatible with this package.
|
||||||
return@filter false
|
return@filter false
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package app.revanced.manager.patcher.patch
|
package app.revanced.manager.patcher.patch
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import app.revanced.patcher.data.ResourceContext
|
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
import app.revanced.patcher.patch.ResourcePatch
|
import app.revanced.patcher.patch.Option as PatchOption
|
||||||
import app.revanced.patcher.patch.options.PatchOption
|
import app.revanced.patcher.patch.resourcePatch
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
|
import kotlin.reflect.KType
|
||||||
|
|
||||||
data class PatchInfo(
|
data class PatchInfo(
|
||||||
val name: String,
|
val name: String,
|
||||||
@ -21,22 +21,26 @@ data class PatchInfo(
|
|||||||
patch.name.orEmpty(),
|
patch.name.orEmpty(),
|
||||||
patch.description,
|
patch.description,
|
||||||
patch.use,
|
patch.use,
|
||||||
patch.compatiblePackages?.map { CompatiblePackage(it) }?.toImmutableList(),
|
patch.compatiblePackages?.map { (pkgName, versions) ->
|
||||||
|
CompatiblePackage(
|
||||||
|
pkgName,
|
||||||
|
versions?.toImmutableSet()
|
||||||
|
)
|
||||||
|
}?.toImmutableList(),
|
||||||
patch.options.map { (_, option) -> Option(option) }.ifEmpty { null }?.toImmutableList()
|
patch.options.map { (_, option) -> Option(option) }.ifEmpty { null }?.toImmutableList()
|
||||||
)
|
)
|
||||||
|
|
||||||
fun compatibleWith(packageName: String) =
|
fun compatibleWith(packageName: String) =
|
||||||
compatiblePackages?.any { it.packageName == packageName } ?: true
|
compatiblePackages?.any { it.packageName == packageName } ?: true
|
||||||
|
|
||||||
fun supportsVersion(packageName: String, versionName: String): Boolean {
|
fun supports(packageName: String, versionName: String?): Boolean {
|
||||||
val packages = compatiblePackages ?: return true // Universal patch
|
val packages = compatiblePackages ?: return true // Universal patch
|
||||||
|
|
||||||
return packages.any { pkg ->
|
return packages.any { pkg ->
|
||||||
if (pkg.packageName != packageName) {
|
if (pkg.packageName != packageName) return@any false
|
||||||
return@any false
|
if (pkg.versions == null) return@any true
|
||||||
}
|
|
||||||
|
|
||||||
pkg.versions == null || pkg.versions.contains(versionName)
|
versionName != null && versionName in pkg.versions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,37 +49,19 @@ data class PatchInfo(
|
|||||||
* The resulting patch cannot be executed.
|
* The resulting patch cannot be executed.
|
||||||
* This is necessary because some functions in ReVanced Library only accept full [Patch] objects.
|
* This is necessary because some functions in ReVanced Library only accept full [Patch] objects.
|
||||||
*/
|
*/
|
||||||
fun toPatcherPatch(): Patch<*> = object : ResourcePatch(
|
fun toPatcherPatch(): Patch<*> =
|
||||||
name = name,
|
resourcePatch(name = name, description = description, use = include) {
|
||||||
description = description,
|
compatiblePackages?.let { pkgs ->
|
||||||
compatiblePackages = compatiblePackages
|
compatibleWith(*pkgs.map { it.packageName to it.versions }.toTypedArray())
|
||||||
?.map(app.revanced.manager.patcher.patch.CompatiblePackage::toPatcherCompatiblePackage)
|
}
|
||||||
?.toSet(),
|
}
|
||||||
use = include,
|
|
||||||
) {
|
|
||||||
override fun execute(context: ResourceContext) =
|
|
||||||
throw Exception("Metadata patches cannot be executed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class CompatiblePackage(
|
data class CompatiblePackage(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val versions: ImmutableSet<String>?
|
val versions: ImmutableSet<String>?
|
||||||
) {
|
)
|
||||||
constructor(pkg: Patch.CompatiblePackage) : this(
|
|
||||||
pkg.name,
|
|
||||||
pkg.versions?.toImmutableSet()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts this [CompatiblePackage] into a [Patch.CompatiblePackage] from patcher.
|
|
||||||
*/
|
|
||||||
fun toPatcherCompatiblePackage() = Patch.CompatiblePackage(
|
|
||||||
name = packageName,
|
|
||||||
versions = versions,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Option<T>(
|
data class Option<T>(
|
||||||
@ -83,7 +69,7 @@ data class Option<T>(
|
|||||||
val key: String,
|
val key: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
val required: Boolean,
|
val required: Boolean,
|
||||||
val type: String,
|
val type: KType,
|
||||||
val default: T?,
|
val default: T?,
|
||||||
val presets: Map<String, T?>?,
|
val presets: Map<String, T?>?,
|
||||||
val validator: (T?) -> Boolean,
|
val validator: (T?) -> Boolean,
|
||||||
@ -93,7 +79,7 @@ data class Option<T>(
|
|||||||
option.key,
|
option.key,
|
||||||
option.description.orEmpty(),
|
option.description.orEmpty(),
|
||||||
option.required,
|
option.required,
|
||||||
option.valueType,
|
option.type,
|
||||||
option.default,
|
option.default,
|
||||||
option.values,
|
option.values,
|
||||||
{ option.validator(option, it) },
|
{ option.validator(option, it) },
|
||||||
|
@ -20,22 +20,20 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onPatchCompleted: () -> Unit,
|
onPatchCompleted: suspend () -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
onProgress: ProgressEventHandler,
|
||||||
) {
|
) {
|
||||||
val bundles = bundles()
|
val bundles = bundles()
|
||||||
|
|
||||||
val selectedBundles = selectedPatches.keys
|
val selectedBundles = selectedPatches.keys
|
||||||
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
||||||
.mapValues { (_, bundle) -> bundle.patchClasses(packageName) }
|
.mapValues { (_, bundle) -> bundle.patches(packageName) }
|
||||||
|
|
||||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||||
allPatches[bundle]?.filter { selected.contains(it.name) }
|
allPatches[bundle]?.filter { selected.contains(it.name) }
|
||||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
|
|
||||||
|
|
||||||
// Set all patch options.
|
// Set all patch options.
|
||||||
options.forEach { (bundle, bundlePatchOptions) ->
|
options.forEach { (bundle, bundlePatchOptions) ->
|
||||||
val patches = allPatches[bundle] ?: return@forEach
|
val patches = allPatches[bundle] ?: return@forEach
|
||||||
@ -53,7 +51,6 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
|||||||
cacheDir,
|
cacheDir,
|
||||||
frameworkPath,
|
frameworkPath,
|
||||||
aaptPath,
|
aaptPath,
|
||||||
enableMultithreadedDexWriter(),
|
|
||||||
context,
|
context,
|
||||||
logger,
|
logger,
|
||||||
File(inputFile),
|
File(inputFile),
|
||||||
@ -62,8 +59,7 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
|||||||
).use { session ->
|
).use { session ->
|
||||||
session.run(
|
session.run(
|
||||||
File(outputFile),
|
File(outputFile),
|
||||||
patchList,
|
patchList
|
||||||
integrations
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,11 +66,11 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onPatchCompleted: () -> Unit,
|
onPatchCompleted: suspend () -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
onProgress: ProgressEventHandler,
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
// Get the location of our own Apk.
|
// Get the location of our own Apk.
|
||||||
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo.sourceDir
|
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
|
||||||
|
|
||||||
val limit = "${prefs.patcherProcessMemoryLimit.get()}M"
|
val limit = "${prefs.patcherProcessMemoryLimit.get()}M"
|
||||||
val propOverride = resolvePropOverride(context)?.absolutePath
|
val propOverride = resolvePropOverride(context)?.absolutePath
|
||||||
@ -123,7 +123,9 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
val eventHandler = object : IPatcherEvents.Stub() {
|
val eventHandler = object : IPatcherEvents.Stub() {
|
||||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||||
|
|
||||||
override fun patchSucceeded() = onPatchCompleted()
|
override fun patchSucceeded() {
|
||||||
|
launch { onPatchCompleted() }
|
||||||
|
}
|
||||||
|
|
||||||
override fun progress(name: String?, state: String?, msg: String?) =
|
override fun progress(name: String?, state: String?, msg: String?) =
|
||||||
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
|
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
|
||||||
@ -148,13 +150,11 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
packageName = packageName,
|
packageName = packageName,
|
||||||
inputFile = inputFile,
|
inputFile = inputFile,
|
||||||
outputFile = outputFile,
|
outputFile = outputFile,
|
||||||
enableMultithrededDexWriter = enableMultithreadedDexWriter(),
|
|
||||||
configurations = selectedPatches.map { (id, patches) ->
|
configurations = selectedPatches.map { (id, patches) ->
|
||||||
val bundle = bundles[id]!!
|
val bundle = bundles[id]!!
|
||||||
|
|
||||||
PatchConfiguration(
|
PatchConfiguration(
|
||||||
bundle.patchesJar.absolutePath,
|
bundle.patchesJar.absolutePath,
|
||||||
bundle.integrations?.absolutePath,
|
|
||||||
patches,
|
patches,
|
||||||
options[id].orEmpty()
|
options[id].orEmpty()
|
||||||
)
|
)
|
||||||
|
@ -26,7 +26,6 @@ sealed class Runtime(context: Context) : KoinComponent {
|
|||||||
context.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
context.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
||||||
|
|
||||||
protected suspend fun bundles() = patchBundlesRepo.bundles.first()
|
protected suspend fun bundles() = patchBundlesRepo.bundles.first()
|
||||||
protected suspend fun enableMultithreadedDexWriter() = prefs.multithreadingDexFileWriter.get()
|
|
||||||
|
|
||||||
abstract suspend fun execute(
|
abstract suspend fun execute(
|
||||||
inputFile: String,
|
inputFile: String,
|
||||||
@ -35,7 +34,7 @@ sealed class Runtime(context: Context) : KoinComponent {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onPatchCompleted: () -> Unit,
|
onPatchCompleted: suspend () -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
onProgress: ProgressEventHandler,
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -12,14 +12,12 @@ data class Parameters(
|
|||||||
val packageName: String,
|
val packageName: String,
|
||||||
val inputFile: String,
|
val inputFile: String,
|
||||||
val outputFile: String,
|
val outputFile: String,
|
||||||
val enableMultithrededDexWriter: Boolean,
|
|
||||||
val configurations: List<PatchConfiguration>,
|
val configurations: List<PatchConfiguration>,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class PatchConfiguration(
|
data class PatchConfiguration(
|
||||||
val bundlePath: String,
|
val bundlePath: String,
|
||||||
val integrationsPath: String?,
|
|
||||||
val patches: Set<String>,
|
val patches: Set<String>,
|
||||||
val options: @RawValue Map<String, Map<String, Any?>>
|
val options: @RawValue Map<String, Map<String, Any?>>
|
||||||
) : Parcelable
|
) : Parcelable
|
@ -54,13 +54,11 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
|
|
||||||
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
||||||
|
|
||||||
val integrations =
|
|
||||||
parameters.configurations.mapNotNull { it.integrationsPath?.let(::File) }
|
|
||||||
val patchList = parameters.configurations.flatMap { config ->
|
val patchList = parameters.configurations.flatMap { config ->
|
||||||
val bundle = PatchBundle(File(config.bundlePath), null)
|
val bundle = PatchBundle(File(config.bundlePath))
|
||||||
|
|
||||||
val patches =
|
val patches =
|
||||||
bundle.patchClasses(parameters.packageName).filter { it.name in config.patches }
|
bundle.patches(parameters.packageName).filter { it.name in config.patches }
|
||||||
.associateBy { it.name }
|
.associateBy { it.name }
|
||||||
|
|
||||||
config.options.forEach { (patchName, opts) ->
|
config.options.forEach { (patchName, opts) ->
|
||||||
@ -81,7 +79,6 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
cacheDir = parameters.cacheDir,
|
cacheDir = parameters.cacheDir,
|
||||||
aaptPath = parameters.aaptPath,
|
aaptPath = parameters.aaptPath,
|
||||||
frameworkDir = parameters.frameworkDir,
|
frameworkDir = parameters.frameworkDir,
|
||||||
multithreadingDexFileWriter = parameters.enableMultithrededDexWriter,
|
|
||||||
androidContext = context,
|
androidContext = context,
|
||||||
logger = logger,
|
logger = logger,
|
||||||
input = File(parameters.inputFile),
|
input = File(parameters.inputFile),
|
||||||
@ -90,7 +87,7 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
events.progress(name, state?.name, message)
|
events.progress(name, state?.name, message)
|
||||||
}
|
}
|
||||||
).use {
|
).use {
|
||||||
it.run(File(parameters.outputFile), patchList, integrations)
|
it.run(File(parameters.outputFile), patchList)
|
||||||
}
|
}
|
||||||
|
|
||||||
events.finished(null)
|
events.finished(null)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package app.revanced.manager.patcher.worker
|
package app.revanced.manager.patcher.worker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
@ -9,9 +10,10 @@ import android.content.Intent
|
|||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Parcelable
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.WindowManager
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.work.ForegroundInfo
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
@ -22,26 +24,33 @@ import app.revanced.manager.domain.installer.RootInstaller
|
|||||||
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.DownloadedAppRepository
|
||||||
|
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.worker.Worker
|
import app.revanced.manager.domain.worker.Worker
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
||||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||||
|
import app.revanced.manager.plugin.downloader.GetScope
|
||||||
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
|
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.ui.model.State
|
||||||
import app.revanced.manager.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.tag
|
import app.revanced.manager.util.tag
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
|
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
|
||||||
|
|
||||||
|
@OptIn(PluginHostApi::class)
|
||||||
class PatcherWorker(
|
class PatcherWorker(
|
||||||
context: Context,
|
context: Context,
|
||||||
parameters: WorkerParameters
|
parameters: WorkerParameters
|
||||||
@ -49,21 +58,23 @@ class PatcherWorker(
|
|||||||
private val workerRepository: WorkerRepository by inject()
|
private val workerRepository: WorkerRepository by inject()
|
||||||
private val prefs: PreferencesManager by inject()
|
private val prefs: PreferencesManager by inject()
|
||||||
private val keystoreManager: KeystoreManager by inject()
|
private val keystoreManager: KeystoreManager by inject()
|
||||||
|
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
|
||||||
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
||||||
private val pm: PM by inject()
|
private val pm: PM by inject()
|
||||||
private val fs: Filesystem by inject()
|
private val fs: Filesystem by inject()
|
||||||
private val installedAppRepository: InstalledAppRepository by inject()
|
private val installedAppRepository: InstalledAppRepository by inject()
|
||||||
private val rootInstaller: RootInstaller by inject()
|
private val rootInstaller: RootInstaller by inject()
|
||||||
|
|
||||||
data class Args(
|
class Args(
|
||||||
val input: SelectedApp,
|
val input: SelectedApp,
|
||||||
val output: String,
|
val output: String,
|
||||||
val selectedPatches: PatchSelection,
|
val selectedPatches: PatchSelection,
|
||||||
val options: Options,
|
val options: Options,
|
||||||
val logger: Logger,
|
val logger: Logger,
|
||||||
val downloadProgress: MutableStateFlow<Pair<Float, Float>?>,
|
val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit,
|
||||||
val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
|
val onPatchCompleted: suspend () -> Unit,
|
||||||
val setInputFile: (File) -> Unit,
|
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
||||||
|
val setInputFile: suspend (File) -> Unit,
|
||||||
val onProgress: ProgressEventHandler
|
val onProgress: ProgressEventHandler
|
||||||
) {
|
) {
|
||||||
val packageName get() = input.packageName
|
val packageName get() = input.packageName
|
||||||
@ -110,7 +121,7 @@ class PatcherWorker(
|
|||||||
|
|
||||||
val wakeLock: PowerManager.WakeLock =
|
val wakeLock: PowerManager.WakeLock =
|
||||||
(applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
(applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||||
.newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, "$tag::Patcher")
|
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::Patcher")
|
||||||
.apply {
|
.apply {
|
||||||
acquire(10 * 60 * 1000L)
|
acquire(10 * 60 * 1000L)
|
||||||
Log.d(tag, "Acquired wakelock.")
|
Log.d(tag, "Acquired wakelock.")
|
||||||
@ -141,20 +152,61 @@ class PatcherWorker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun download(plugin: LoadedDownloaderPlugin, data: Parcelable) =
|
||||||
|
downloadedAppRepository.download(
|
||||||
|
plugin,
|
||||||
|
data,
|
||||||
|
args.packageName,
|
||||||
|
args.input.version,
|
||||||
|
onDownload = args.onDownloadProgress
|
||||||
|
).also {
|
||||||
|
args.setInputFile(it)
|
||||||
|
updateProgress(state = State.COMPLETED) // Download APK
|
||||||
|
}
|
||||||
|
|
||||||
val inputFile = when (val selectedApp = args.input) {
|
val inputFile = when (val selectedApp = args.input) {
|
||||||
is SelectedApp.Download -> {
|
is SelectedApp.Download -> {
|
||||||
downloadedAppRepository.download(
|
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
|
||||||
selectedApp.app,
|
|
||||||
prefs.preferSplits.get(),
|
download(plugin, data)
|
||||||
onDownload = { args.downloadProgress.emit(it) }
|
}
|
||||||
).also {
|
|
||||||
args.setInputFile(it)
|
is SelectedApp.Search -> {
|
||||||
updateProgress(state = State.COMPLETED) // Download APK
|
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||||
}
|
.firstNotNullOfOrNull { plugin ->
|
||||||
|
try {
|
||||||
|
val getScope = object : GetScope {
|
||||||
|
override val pluginPackageName = plugin.packageName
|
||||||
|
override val hostPackageName = applicationContext.packageName
|
||||||
|
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||||
|
val result = args.handleStartActivityRequest(plugin, intent)
|
||||||
|
return when (result.resultCode) {
|
||||||
|
Activity.RESULT_OK -> result.data
|
||||||
|
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||||
|
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||||
|
result.resultCode,
|
||||||
|
result.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
plugin.get(
|
||||||
|
getScope,
|
||||||
|
selectedApp.packageName,
|
||||||
|
selectedApp.version
|
||||||
|
)
|
||||||
|
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
||||||
|
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||||
|
throw e
|
||||||
|
} catch (_: UserInteractionException) {
|
||||||
|
null
|
||||||
|
}?.let { (data, _) -> download(plugin, data) }
|
||||||
|
} ?: throw Exception("App is not available.")
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
||||||
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir)
|
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
val runtime = if (prefs.useProcessRuntime.get()) {
|
val runtime = if (prefs.useProcessRuntime.get()) {
|
||||||
@ -170,11 +222,7 @@ class PatcherWorker(
|
|||||||
args.selectedPatches,
|
args.selectedPatches,
|
||||||
args.options,
|
args.options,
|
||||||
args.logger,
|
args.logger,
|
||||||
onPatchCompleted = {
|
args.onPatchCompleted,
|
||||||
args.patchesProgress.update { (completed, total) ->
|
|
||||||
completed + 1 to total
|
|
||||||
}
|
|
||||||
},
|
|
||||||
args.onProgress
|
args.onProgress
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -184,7 +232,10 @@ class PatcherWorker(
|
|||||||
Log.i(tag, "Patching succeeded".logFmt())
|
Log.i(tag, "Patching succeeded".logFmt())
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: ProcessRuntime.RemoteFailureException) {
|
} catch (e: ProcessRuntime.RemoteFailureException) {
|
||||||
Log.e(tag, "An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt())
|
Log.e(
|
||||||
|
tag,
|
||||||
|
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
|
||||||
|
)
|
||||||
updateProgress(state = State.FAILED, message = e.originalStackTrace)
|
updateProgress(state = State.FAILED, message = e.originalStackTrace)
|
||||||
Result.failure()
|
Result.failure()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -8,7 +8,6 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.outlined.Source
|
import androidx.compose.material.icons.outlined.Source
|
||||||
import androidx.compose.material.icons.outlined.Update
|
import androidx.compose.material.icons.outlined.Update
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
@ -24,6 +23,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
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
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||||
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AutoUpdatesDialog(onSubmit: (Boolean, Boolean) -> Unit) {
|
fun AutoUpdatesDialog(onSubmit: (Boolean, Boolean) -> Unit) {
|
||||||
@ -76,6 +77,7 @@ private fun AutoUpdatesItem(
|
|||||||
) = ListItem(
|
) = ListItem(
|
||||||
leadingContent = { Icon(icon, null) },
|
leadingContent = { Icon(icon, null) },
|
||||||
headlineContent = { Text(stringResource(headline)) },
|
headlineContent = { Text(stringResource(headline)) },
|
||||||
trailingContent = { Checkbox(checked = checked, onCheckedChange = null) },
|
trailingContent = { HapticCheckbox(checked = checked, onCheckedChange = null) },
|
||||||
modifier = Modifier.clickable { onCheckedChange(!checked) }
|
modifier = Modifier.clickable { onCheckedChange(!checked) },
|
||||||
|
colors = transparentListItemColors
|
||||||
)
|
)
|
@ -12,10 +12,12 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
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.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||||
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AvailableUpdateDialog(
|
fun AvailableUpdateDialog(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
@ -69,10 +71,11 @@ fun AvailableUpdateDialog(
|
|||||||
Text(stringResource(R.string.never_show_again))
|
Text(stringResource(R.string.never_show_again))
|
||||||
},
|
},
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
|
||||||
Checkbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it })
|
HapticCheckbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it })
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
colors = transparentListItemColors
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandIn
|
||||||
|
import androidx.compose.animation.shrinkOut
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Done
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SelectableChipColors
|
||||||
|
import androidx.compose.material3.SelectableChipElevation
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CheckedFilterChip(
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
label: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
shape: Shape = FilterChipDefaults.shape,
|
||||||
|
colors: SelectableChipColors = FilterChipDefaults.filterChipColors(),
|
||||||
|
elevation: SelectableChipElevation? = FilterChipDefaults.filterChipElevation(),
|
||||||
|
border: BorderStroke? = FilterChipDefaults.filterChipBorder(enabled, selected),
|
||||||
|
interactionSource: MutableInteractionSource? = null
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick,
|
||||||
|
label = label,
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
leadingIcon = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = selected,
|
||||||
|
enter = expandIn(expandFrom = Alignment.CenterStart),
|
||||||
|
exit = shrinkOut(shrinkTowards = Alignment.CenterStart)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(FilterChipDefaults.IconSize),
|
||||||
|
imageVector = Icons.Filled.Done,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingIcon = trailingIcon,
|
||||||
|
shape = shape,
|
||||||
|
colors = colors,
|
||||||
|
elevation = elevation,
|
||||||
|
border = border,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.Share
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
dismissOnBackPress = true
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
BundleTopBar(
|
||||||
|
title = stringResource(R.string.bundle_error),
|
||||||
|
onBackClick = onDismiss,
|
||||||
|
backIcon = {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
stringResource(R.string.back)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
val sendIntent: Intent = Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
text
|
||||||
|
)
|
||||||
|
type = "text/plain"
|
||||||
|
}
|
||||||
|
|
||||||
|
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||||
|
context.startActivity(shareIntent)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.Share,
|
||||||
|
contentDescription = stringResource(R.string.share)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
ColumnWithScrollbar(
|
||||||
|
modifier = Modifier.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
Text(text, modifier = Modifier.horizontalScroll(rememberScrollState()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,6 @@ import androidx.annotation.StringRes
|
|||||||
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.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Check
|
|
||||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@ -21,35 +20,25 @@ 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
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.ui.model.InstallerModel
|
||||||
import com.github.materiiapps.enumutil.FromValue
|
import com.github.materiiapps.enumutil.FromValue
|
||||||
|
|
||||||
private typealias InstallerStatusDialogButtonHandler = ((model: InstallerModel) -> Unit)
|
private typealias InstallerStatusDialogButtonHandler = ((model: InstallerModel) -> Unit)
|
||||||
private typealias InstallerStatusDialogButton = @Composable (model: InstallerStatusDialogModel) -> Unit
|
private typealias InstallerStatusDialogButton = @Composable (model: InstallerModel, dismiss: () -> Unit) -> Unit
|
||||||
|
|
||||||
interface InstallerModel {
|
|
||||||
fun reinstall()
|
|
||||||
fun install()
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InstallerStatusDialogModel : InstallerModel {
|
|
||||||
var packageInstallerStatus: Int?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InstallerStatusDialog(model: InstallerStatusDialogModel) {
|
fun InstallerStatusDialog(installerStatus: Int, model: InstallerModel, onDismiss: () -> Unit) {
|
||||||
val dialogKind = remember {
|
val dialogKind = remember {
|
||||||
DialogKind.fromValue(model.packageInstallerStatus!!) ?: DialogKind.FAILURE
|
DialogKind.fromValue(installerStatus) ?: DialogKind.FAILURE
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = {
|
onDismissRequest = onDismiss,
|
||||||
model.packageInstallerStatus = null
|
|
||||||
},
|
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
dialogKind.confirmButton(model)
|
dialogKind.confirmButton(model, onDismiss)
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
dialogKind.dismissButton?.invoke(model)
|
dialogKind.dismissButton?.invoke(model, onDismiss)
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
Icon(dialogKind.icon, null)
|
Icon(dialogKind.icon, null)
|
||||||
@ -75,10 +64,10 @@ fun InstallerStatusDialog(model: InstallerStatusDialogModel) {
|
|||||||
private fun installerStatusDialogButton(
|
private fun installerStatusDialogButton(
|
||||||
@StringRes buttonStringResId: Int,
|
@StringRes buttonStringResId: Int,
|
||||||
buttonHandler: InstallerStatusDialogButtonHandler = { },
|
buttonHandler: InstallerStatusDialogButtonHandler = { },
|
||||||
): InstallerStatusDialogButton = { model ->
|
): InstallerStatusDialogButton = { model, dismiss ->
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
model.packageInstallerStatus = null
|
dismiss()
|
||||||
buttonHandler(model)
|
buttonHandler(model)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@ -154,6 +143,7 @@ enum class DialogKind(
|
|||||||
model.install()
|
model.install()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Needed due to the @FromValue annotation.
|
// Needed due to the @FromValue annotation.
|
||||||
companion object
|
companion object
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SearchBar
|
||||||
|
import androidx.compose.material3.SearchBarColors
|
||||||
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SearchBar(
|
||||||
|
query: String,
|
||||||
|
onQueryChange: (String) -> Unit,
|
||||||
|
expanded: Boolean,
|
||||||
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
|
placeholder: (@Composable () -> Unit)? = null,
|
||||||
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val colors = SearchBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
dividerColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
SearchBar(
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
inputField = {
|
||||||
|
SearchBarDefaults.InputField(
|
||||||
|
modifier = Modifier.sizeIn(minWidth = 380.dp),
|
||||||
|
query = query,
|
||||||
|
onQueryChange = onQueryChange,
|
||||||
|
onSearch = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
},
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = onExpandedChange,
|
||||||
|
placeholder = placeholder,
|
||||||
|
leadingIcon = leadingIcon,
|
||||||
|
trailingIcon = trailingIcon
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = onExpandedChange,
|
||||||
|
colors = colors,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,15 @@
|
|||||||
package app.revanced.manager.ui.component
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SearchBar
|
import androidx.compose.material3.SearchBar
|
||||||
|
import androidx.compose.material3.SearchBarColors
|
||||||
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -27,29 +29,38 @@ fun SearchView(
|
|||||||
placeholder: (@Composable () -> Unit)? = null,
|
placeholder: (@Composable () -> Unit)? = null,
|
||||||
content: @Composable ColumnScope.() -> Unit
|
content: @Composable ColumnScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
|
val colors = SearchBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
dividerColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
SearchBar(
|
SearchBar(
|
||||||
query = query,
|
inputField = {
|
||||||
onQueryChange = onQueryChange,
|
SearchBarDefaults.InputField(
|
||||||
onSearch = {
|
query = query,
|
||||||
keyboardController?.hide()
|
onQueryChange = onQueryChange,
|
||||||
},
|
onSearch = {
|
||||||
active = true,
|
keyboardController?.hide()
|
||||||
onActiveChange = onActiveChange,
|
},
|
||||||
modifier = Modifier
|
expanded = true,
|
||||||
.fillMaxSize()
|
onExpandedChange = onActiveChange,
|
||||||
.focusRequester(focusRequester),
|
placeholder = placeholder,
|
||||||
placeholder = placeholder,
|
leadingIcon = {
|
||||||
leadingIcon = {
|
IconButton(onClick = { onActiveChange(false) }) {
|
||||||
IconButton({ onActiveChange(false) }) {
|
Icon(
|
||||||
Icon(
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
stringResource(R.string.back)
|
||||||
stringResource(R.string.back)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
expanded = true,
|
||||||
|
onExpandedChange = onActiveChange,
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
colors = colors,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.TextInputDialog
|
import app.revanced.manager.ui.component.TextInputDialog
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BaseBundleDialog(
|
fun BaseBundleDialog(
|
||||||
@ -89,7 +90,7 @@ fun BaseBundleDialog(
|
|||||||
headlineText = stringResource(R.string.bundle_auto_update),
|
headlineText = stringResource(R.string.bundle_auto_update),
|
||||||
supportingText = stringResource(R.string.bundle_auto_update_description),
|
supportingText = stringResource(R.string.bundle_auto_update_description),
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
HapticSwitch(
|
||||||
checked = autoUpdate,
|
checked = autoUpdate,
|
||||||
onCheckedChange = onAutoUpdateChange
|
onCheckedChange = onAutoUpdateChange
|
||||||
)
|
)
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
package app.revanced.manager.ui.component.bundle
|
package app.revanced.manager.ui.component.bundle
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.horizontalScroll
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||||
import androidx.compose.material.icons.outlined.Share
|
|
||||||
import androidx.compose.material.icons.outlined.Update
|
import androidx.compose.material.icons.outlined.Update
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
@ -26,7 +21,7 @@ import app.revanced.manager.domain.bundles.PatchBundleSource
|
|||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@ -104,7 +99,7 @@ fun BundleInformationDialog(
|
|||||||
name = bundleName,
|
name = bundleName,
|
||||||
remoteUrl = bundle.asRemoteOrNull?.endpoint,
|
remoteUrl = bundle.asRemoteOrNull?.endpoint,
|
||||||
patchCount = patchCount,
|
patchCount = patchCount,
|
||||||
version = props?.versionInfo?.patches,
|
version = props?.version,
|
||||||
autoUpdate = props?.autoUpdate ?: false,
|
autoUpdate = props?.autoUpdate ?: false,
|
||||||
onAutoUpdateChange = {
|
onAutoUpdateChange = {
|
||||||
composableScope.launch {
|
composableScope.launch {
|
||||||
@ -119,7 +114,7 @@ fun BundleInformationDialog(
|
|||||||
var showDialog by rememberSaveable {
|
var showDialog by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
if (showDialog) BundleErrorViewerDialog(
|
if (showDialog) ExceptionViewerDialog(
|
||||||
onDismiss = { showDialog = false },
|
onDismiss = { showDialog = false },
|
||||||
text = remember(it) { it.stackTraceToString() }
|
text = remember(it) { it.stackTraceToString() }
|
||||||
)
|
)
|
||||||
@ -149,60 +144,3 @@ fun BundleInformationDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun BundleErrorViewerDialog(onDismiss: () -> Unit, text: String) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
properties = DialogProperties(
|
|
||||||
usePlatformDefaultWidth = false,
|
|
||||||
dismissOnBackPress = true
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
BundleTopBar(
|
|
||||||
title = stringResource(R.string.bundle_error),
|
|
||||||
onBackClick = onDismiss,
|
|
||||||
backIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.back)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
val sendIntent: Intent = Intent().apply {
|
|
||||||
action = Intent.ACTION_SEND
|
|
||||||
putExtra(
|
|
||||||
Intent.EXTRA_TEXT,
|
|
||||||
text
|
|
||||||
)
|
|
||||||
type = "text/plain"
|
|
||||||
}
|
|
||||||
|
|
||||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
|
||||||
context.startActivity(shareIntent)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Outlined.Share,
|
|
||||||
contentDescription = stringResource(R.string.share)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
ColumnWithScrollbar(
|
|
||||||
modifier = Modifier.padding(paddingValues)
|
|
||||||
) {
|
|
||||||
Text(text, modifier = Modifier.horizontalScroll(rememberScrollState()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||||
import androidx.compose.material.icons.outlined.Warning
|
import androidx.compose.material.icons.outlined.Warning
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -27,6 +26,7 @@ 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.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ fun BundleItem(
|
|||||||
val state by bundle.state.collectAsStateWithLifecycle()
|
val state by bundle.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val version by remember(bundle) {
|
val version by remember(bundle) {
|
||||||
bundle.propsFlow().map { props -> props?.versionInfo?.patches }
|
bundle.propsFlow().map { props -> props?.version }
|
||||||
}.collectAsStateWithLifecycle(null)
|
}.collectAsStateWithLifecycle(null)
|
||||||
val name by bundle.nameState
|
val name by bundle.nameState
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ fun BundleItem(
|
|||||||
),
|
),
|
||||||
leadingContent = if (selectable) {
|
leadingContent = if (selectable) {
|
||||||
{
|
{
|
||||||
Checkbox(
|
HapticCheckbox(
|
||||||
checked = isBundleSelected,
|
checked = isBundleSelected,
|
||||||
onCheckedChange = toggleSelection,
|
onCheckedChange = toggleSelection,
|
||||||
)
|
)
|
||||||
|
@ -10,47 +10,32 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Topic
|
import androidx.compose.material.icons.filled.Topic
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.RadioButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
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.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||||
import app.revanced.manager.ui.component.TextHorizontalPadding
|
import app.revanced.manager.ui.component.TextHorizontalPadding
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
||||||
import app.revanced.manager.ui.model.BundleType
|
import app.revanced.manager.ui.model.BundleType
|
||||||
import app.revanced.manager.util.APK_MIMETYPE
|
import app.revanced.manager.util.BIN_MIMETYPE
|
||||||
import app.revanced.manager.util.JAR_MIMETYPE
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ImportPatchBundleDialog(
|
fun ImportPatchBundleDialog(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onRemoteSubmit: (String, Boolean) -> Unit,
|
onRemoteSubmit: (String, Boolean) -> Unit,
|
||||||
onLocalSubmit: (Uri, Uri?) -> Unit
|
onLocalSubmit: (Uri) -> Unit
|
||||||
) {
|
) {
|
||||||
var currentStep by rememberSaveable { mutableIntStateOf(0) }
|
var currentStep by rememberSaveable { mutableIntStateOf(0) }
|
||||||
var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) }
|
var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) }
|
||||||
var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
|
var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||||
var integrations by rememberSaveable { mutableStateOf<Uri?>(null) }
|
|
||||||
var remoteUrl by rememberSaveable { mutableStateOf("") }
|
var remoteUrl by rememberSaveable { mutableStateOf("") }
|
||||||
var autoUpdate by rememberSaveable { mutableStateOf(false) }
|
var autoUpdate by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
@ -60,16 +45,7 @@ fun ImportPatchBundleDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun launchPatchActivity() {
|
fun launchPatchActivity() {
|
||||||
patchActivityLauncher.launch(JAR_MIMETYPE)
|
patchActivityLauncher.launch(BIN_MIMETYPE)
|
||||||
}
|
|
||||||
|
|
||||||
val integrationsActivityLauncher =
|
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
|
||||||
uri?.let { integrations = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchIntegrationsActivity() {
|
|
||||||
integrationsActivityLauncher.launch(APK_MIMETYPE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val steps = listOf<@Composable () -> Unit>(
|
val steps = listOf<@Composable () -> Unit>(
|
||||||
@ -82,11 +58,9 @@ fun ImportPatchBundleDialog(
|
|||||||
ImportBundleStep(
|
ImportBundleStep(
|
||||||
bundleType,
|
bundleType,
|
||||||
patchBundle,
|
patchBundle,
|
||||||
integrations,
|
|
||||||
remoteUrl,
|
remoteUrl,
|
||||||
autoUpdate,
|
autoUpdate,
|
||||||
{ launchPatchActivity() },
|
{ launchPatchActivity() },
|
||||||
{ launchIntegrationsActivity() },
|
|
||||||
{ remoteUrl = it },
|
{ remoteUrl = it },
|
||||||
{ autoUpdate = it }
|
{ autoUpdate = it }
|
||||||
)
|
)
|
||||||
@ -114,13 +88,7 @@ fun ImportPatchBundleDialog(
|
|||||||
enabled = inputsAreValid,
|
enabled = inputsAreValid,
|
||||||
onClick = {
|
onClick = {
|
||||||
when (bundleType) {
|
when (bundleType) {
|
||||||
BundleType.Local -> patchBundle?.let {
|
BundleType.Local -> patchBundle?.let(onLocalSubmit)
|
||||||
onLocalSubmit(
|
|
||||||
it,
|
|
||||||
integrations
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BundleType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate)
|
BundleType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,11 +138,12 @@ fun SelectBundleTypeStep(
|
|||||||
overlineContent = { Text(stringResource(R.string.recommended)) },
|
overlineContent = { Text(stringResource(R.string.recommended)) },
|
||||||
supportingContent = { Text(stringResource(R.string.remote_bundle_description)) },
|
supportingContent = { Text(stringResource(R.string.remote_bundle_description)) },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
RadioButton(
|
HapticRadioButton(
|
||||||
selected = bundleType == BundleType.Remote,
|
selected = bundleType == BundleType.Remote,
|
||||||
onClick = null
|
onClick = null
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
colors = transparentListItemColors
|
||||||
)
|
)
|
||||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||||
ListItem(
|
ListItem(
|
||||||
@ -186,11 +155,12 @@ fun SelectBundleTypeStep(
|
|||||||
supportingContent = { Text(stringResource(R.string.local_bundle_description)) },
|
supportingContent = { Text(stringResource(R.string.local_bundle_description)) },
|
||||||
overlineContent = { },
|
overlineContent = { },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
RadioButton(
|
HapticRadioButton(
|
||||||
selected = bundleType == BundleType.Local,
|
selected = bundleType == BundleType.Local,
|
||||||
onClick = null
|
onClick = null
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
colors = transparentListItemColors
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,11 +171,9 @@ fun SelectBundleTypeStep(
|
|||||||
fun ImportBundleStep(
|
fun ImportBundleStep(
|
||||||
bundleType: BundleType,
|
bundleType: BundleType,
|
||||||
patchBundle: Uri?,
|
patchBundle: Uri?,
|
||||||
integrations: Uri?,
|
|
||||||
remoteUrl: String,
|
remoteUrl: String,
|
||||||
autoUpdate: Boolean,
|
autoUpdate: Boolean,
|
||||||
launchPatchActivity: () -> Unit,
|
launchPatchActivity: () -> Unit,
|
||||||
launchIntegrationsActivity: () -> Unit,
|
|
||||||
onRemoteUrlChange: (String) -> Unit,
|
onRemoteUrlChange: (String) -> Unit,
|
||||||
onAutoUpdateChange: (Boolean) -> Unit
|
onAutoUpdateChange: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
@ -225,19 +193,8 @@ fun ImportBundleStep(
|
|||||||
Icon(imageVector = Icons.Default.Topic, contentDescription = null)
|
Icon(imageVector = Icons.Default.Topic, contentDescription = null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.clickable { launchPatchActivity() }
|
modifier = Modifier.clickable { launchPatchActivity() },
|
||||||
)
|
colors = transparentListItemColors
|
||||||
ListItem(
|
|
||||||
headlineContent = {
|
|
||||||
Text(stringResource(R.string.integrations_field))
|
|
||||||
},
|
|
||||||
supportingContent = { Text(stringResource(if (integrations != null) R.string.file_field_set else R.string.file_field_not_set)) },
|
|
||||||
trailingContent = {
|
|
||||||
IconButton(onClick = launchIntegrationsActivity) {
|
|
||||||
Icon(imageVector = Icons.Default.Topic, contentDescription = null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.clickable { launchIntegrationsActivity() }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,8 +219,8 @@ fun ImportBundleStep(
|
|||||||
),
|
),
|
||||||
headlineContent = { Text(stringResource(R.string.auto_update)) },
|
headlineContent = { Text(stringResource(R.string.auto_update)) },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
|
||||||
Checkbox(
|
HapticCheckbox(
|
||||||
checked = autoUpdate,
|
checked = autoUpdate,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
onAutoUpdateChange(!autoUpdate)
|
onAutoUpdateChange(!autoUpdate)
|
||||||
@ -271,6 +228,7 @@ fun ImportBundleStep(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
colors = transparentListItemColors
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
package app.revanced.manager.ui.component.haptics
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.CheckboxColors
|
||||||
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import app.revanced.manager.util.withHapticFeedback
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HapticCheckbox(
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: ((Boolean) -> Unit)?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
colors: CheckboxColors = CheckboxDefaults.colors(),
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange?.withHapticFeedback(HapticFeedbackConstants.CLOCK_TICK),
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
colors = colors,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package app.revanced.manager.ui.component.haptics
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
|
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||||
|
import androidx.compose.material3.FloatingActionButtonElevation
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import app.revanced.manager.util.withHapticFeedback
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HapticExtendedFloatingActionButton (
|
||||||
|
text: @Composable () -> Unit,
|
||||||
|
icon: @Composable () -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
expanded: Boolean = true,
|
||||||
|
shape: Shape = FloatingActionButtonDefaults.extendedFabShape,
|
||||||
|
containerColor: Color = FloatingActionButtonDefaults.containerColor,
|
||||||
|
contentColor: Color = contentColorFor(containerColor),
|
||||||
|
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
text = text,
|
||||||
|
icon = icon,
|
||||||
|
onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY),
|
||||||
|
modifier = modifier,
|
||||||
|
expanded = expanded,
|
||||||
|
shape = shape,
|
||||||
|
containerColor = containerColor,
|
||||||
|
contentColor = contentColor,
|
||||||
|
elevation = elevation,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package app.revanced.manager.ui.component.haptics
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||||
|
import androidx.compose.material3.FloatingActionButtonElevation
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import app.revanced.manager.util.withHapticFeedback
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HapticFloatingActionButton (
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
shape: Shape = FloatingActionButtonDefaults.shape,
|
||||||
|
containerColor: Color = FloatingActionButtonDefaults.containerColor,
|
||||||
|
contentColor: Color = contentColorFor(containerColor),
|
||||||
|
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY),
|
||||||
|
modifier = modifier,
|
||||||
|
shape = shape,
|
||||||
|
containerColor = containerColor,
|
||||||
|
contentColor = contentColor,
|
||||||
|
elevation = elevation,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package app.revanced.manager.ui.component.haptics
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.RadioButtonColors
|
||||||
|
import androidx.compose.material3.RadioButtonDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HapticRadioButton(
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: (() -> Unit)?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
colors: RadioButtonColors = RadioButtonDefaults.colors(),
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
|
||||||
|
RadioButton(
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick?.let {
|
||||||
|
{
|
||||||
|
// Perform haptic feedback
|
||||||
|
if (!selected) view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
colors = colors,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package app.revanced.manager.ui.component.haptics
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchColors
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HapticSwitch(
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
thumbContent: (@Composable () -> Unit)? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
colors: SwitchColors = SwitchDefaults.colors(),
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) {
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = { newChecked ->
|
||||||
|
val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
||||||
|
when {
|
||||||
|
newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON
|
||||||
|
newChecked -> HapticFeedbackConstants.VIRTUAL_KEY
|
||||||
|
!newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF
|
||||||
|
!newChecked -> HapticFeedbackConstants.CLOCK_TICK
|
||||||
|
}
|
||||||
|
onCheckedChange(newChecked)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
thumbContent = thumbContent,
|
||||||
|
enabled = enabled,
|
||||||
|
colors = colors,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package app.revanced.manager.ui.component.haptics
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import app.revanced.manager.util.withHapticFeedback
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HapticTab (
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
text: @Composable (() -> Unit)? = null,
|
||||||
|
icon: @Composable (() -> Unit)? = null,
|
||||||
|
selectedContentColor: Color = LocalContentColor.current,
|
||||||
|
unselectedContentColor: Color = selectedContentColor,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
Tab(
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY),
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
text = text,
|
||||||
|
icon = icon,
|
||||||
|
selectedContentColor = selectedContentColor,
|
||||||
|
unselectedContentColor = unselectedContentColor,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
}
|
@ -2,12 +2,7 @@ package app.revanced.manager.ui.component.patcher
|
|||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.RadioButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
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
|
||||||
@ -17,6 +12,8 @@ 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.data.room.apps.installed.InstallType
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
||||||
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InstallPickerDialog(
|
fun InstallPickerDialog(
|
||||||
@ -45,16 +42,17 @@ fun InstallPickerDialog(
|
|||||||
title = { Text(stringResource(R.string.select_install_type)) },
|
title = { Text(stringResource(R.string.select_install_type)) },
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
InstallType.values().forEach {
|
InstallType.entries.forEach {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { selectedInstallType = it },
|
modifier = Modifier.clickable { selectedInstallType = it },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
RadioButton(
|
HapticRadioButton(
|
||||||
selected = selectedInstallType == it,
|
selected = selectedInstallType == it,
|
||||||
onClick = null
|
onClick = null
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
headlineContent = { Text(stringResource(it.stringResource)) }
|
headlineContent = { Text(stringResource(it.stringResource)) },
|
||||||
|
colors = transparentListItemColors
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,13 +36,15 @@ import androidx.compose.ui.semantics.semantics
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.ArrowButton
|
import app.revanced.manager.ui.component.ArrowButton
|
||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
|
import app.revanced.manager.ui.model.ProgressKey
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.ui.model.State
|
||||||
import app.revanced.manager.ui.model.Step
|
import app.revanced.manager.ui.model.Step
|
||||||
import app.revanced.manager.ui.model.StepCategory
|
import app.revanced.manager.ui.model.StepCategory
|
||||||
|
import app.revanced.manager.ui.model.StepProgressProvider
|
||||||
|
import java.util.Locale
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
// Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt
|
// Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt
|
||||||
@ -51,6 +53,7 @@ fun Steps(
|
|||||||
category: StepCategory,
|
category: StepCategory,
|
||||||
steps: List<Step>,
|
steps: List<Step>,
|
||||||
stepCount: Pair<Int, Int>? = null,
|
stepCount: Pair<Int, Int>? = null,
|
||||||
|
stepProgressProvider: StepProgressProvider
|
||||||
) {
|
) {
|
||||||
var expanded by rememberSaveable { mutableStateOf(true) }
|
var expanded by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
@ -115,13 +118,20 @@ fun Steps(
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
steps.forEach { step ->
|
steps.forEach { step ->
|
||||||
val downloadProgress = step.downloadProgress?.collectAsStateWithLifecycle()
|
val (progress, progressText) = when (step.progressKey) {
|
||||||
|
null -> null
|
||||||
|
ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) ->
|
||||||
|
if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
|
||||||
|
else null to "${downloaded.megaBytes} MB"
|
||||||
|
}
|
||||||
|
} ?: (null to null)
|
||||||
|
|
||||||
SubStep(
|
SubStep(
|
||||||
name = step.name,
|
name = step.name,
|
||||||
state = step.state,
|
state = step.state,
|
||||||
message = step.message,
|
message = step.message,
|
||||||
downloadProgress = downloadProgress?.value
|
progress = progress,
|
||||||
|
progressText = progressText
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,7 +144,8 @@ fun SubStep(
|
|||||||
name: String,
|
name: String,
|
||||||
state: State,
|
state: State,
|
||||||
message: String? = null,
|
message: String? = null,
|
||||||
downloadProgress: Pair<Float, Float>? = null
|
progress: Float? = null,
|
||||||
|
progressText: String? = null
|
||||||
) {
|
) {
|
||||||
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
@ -155,7 +166,7 @@ fun SubStep(
|
|||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
StepIcon(state, downloadProgress, size = 20.dp)
|
StepIcon(state, progress, size = 20.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@ -166,8 +177,8 @@ fun SubStep(
|
|||||||
modifier = Modifier.weight(1f, true),
|
modifier = Modifier.weight(1f, true),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (message != null) {
|
when {
|
||||||
Box(
|
message != null -> Box(
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
@ -177,13 +188,11 @@ fun SubStep(
|
|||||||
onClick = null
|
onClick = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
downloadProgress?.let { (current, total) ->
|
progressText != null -> Text(
|
||||||
Text(
|
progressText,
|
||||||
"$current/$total MB",
|
style = MaterialTheme.typography.labelSmall
|
||||||
style = MaterialTheme.typography.labelSmall
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +208,7 @@ fun SubStep(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StepIcon(state: State, progress: Pair<Float, Float>? = null, size: Dp) {
|
fun StepIcon(state: State, progress: Float? = null, size: Dp) {
|
||||||
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
||||||
|
|
||||||
when (state) {
|
when (state) {
|
||||||
@ -233,8 +242,10 @@ fun StepIcon(state: State, progress: Pair<Float, Float>? = null, size: Dp) {
|
|||||||
contentDescription = description
|
contentDescription = description
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
progress = { progress?.let { (current, total) -> current / total } },
|
progress = { progress },
|
||||||
strokeWidth = strokeWidth
|
strokeWidth = strokeWidth
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val Long.megaBytes get() = "%.1f".format(locale = Locale.ROOT, toDouble() / 1_000_000)
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.LocalIndication
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@ -20,58 +21,37 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.DragHandle
|
import androidx.compose.material.icons.filled.DragHandle
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material.icons.outlined.Edit
|
|
||||||
import androidx.compose.material.icons.outlined.Folder
|
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
|
||||||
import androidx.compose.material.icons.outlined.Restore
|
|
||||||
import androidx.compose.material.icons.outlined.SelectAll
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.RadioButton
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisallowComposableCalls
|
import androidx.compose.runtime.DisallowComposableCalls
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.setValue
|
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.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog as ComposeDialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.platform.Filesystem
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.patcher.patch.Option
|
import app.revanced.manager.patcher.patch.Option
|
||||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
import app.revanced.manager.ui.component.*
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.FloatInputDialog
|
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
||||||
import app.revanced.manager.ui.component.IntInputDialog
|
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||||
import app.revanced.manager.ui.component.LongInputDialog
|
|
||||||
import app.revanced.manager.util.isScrollingUp
|
import app.revanced.manager.util.isScrollingUp
|
||||||
import app.revanced.manager.util.mutableStateSetOf
|
import app.revanced.manager.util.mutableStateSetOf
|
||||||
import app.revanced.manager.util.saver.snapshotStateListSaver
|
import app.revanced.manager.util.saver.snapshotStateListSaver
|
||||||
import app.revanced.manager.util.saver.snapshotStateSetSaver
|
import app.revanced.manager.util.saver.snapshotStateSetSaver
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@ -80,6 +60,8 @@ import sh.calvin.reorderable.ReorderableItem
|
|||||||
import sh.calvin.reorderable.rememberReorderableLazyColumnState
|
import sh.calvin.reorderable.rememberReorderableLazyColumnState
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
import kotlin.reflect.typeOf
|
||||||
|
import androidx.compose.ui.window.Dialog as ComposeDialog
|
||||||
|
|
||||||
private class OptionEditorScope<T : Any>(
|
private class OptionEditorScope<T : Any>(
|
||||||
private val editor: OptionEditor<T>,
|
private val editor: OptionEditor<T>,
|
||||||
@ -117,17 +99,17 @@ private interface OptionEditor<T : Any> {
|
|||||||
fun Dialog(scope: OptionEditorScope<T>)
|
fun Dialog(scope: OptionEditorScope<T>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T : Serializable> OptionEditor<T>.toMapEditorElements() = arrayOf(
|
||||||
|
typeOf<T>() to this,
|
||||||
|
typeOf<List<T>>() to ListOptionEditor(this)
|
||||||
|
)
|
||||||
|
|
||||||
private val optionEditors = mapOf(
|
private val optionEditors = mapOf(
|
||||||
"Boolean" to BooleanOptionEditor,
|
*BooleanOptionEditor.toMapEditorElements(),
|
||||||
"String" to StringOptionEditor,
|
*StringOptionEditor.toMapEditorElements(),
|
||||||
"Int" to IntOptionEditor,
|
*IntOptionEditor.toMapEditorElements(),
|
||||||
"Long" to LongOptionEditor,
|
*LongOptionEditor.toMapEditorElements(),
|
||||||
"Float" to FloatOptionEditor,
|
*FloatOptionEditor.toMapEditorElements()
|
||||||
"BooleanArray" to ListOptionEditor(BooleanOptionEditor),
|
|
||||||
"StringArray" to ListOptionEditor(StringOptionEditor),
|
|
||||||
"IntArray" to ListOptionEditor(IntOptionEditor),
|
|
||||||
"LongArray" to ListOptionEditor(LongOptionEditor),
|
|
||||||
"FloatArray" to ListOptionEditor(FloatOptionEditor),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -160,13 +142,19 @@ private inline fun <T : Any> WithOptionEditor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T : Any> OptionItem(option: Option<T>, value: T?, setValue: (T?) -> Unit) {
|
fun <T : Any> OptionItem(
|
||||||
|
option: Option<T>,
|
||||||
|
value: T?,
|
||||||
|
setValue: (T?) -> Unit,
|
||||||
|
) {
|
||||||
val editor = remember(option.type, option.presets) {
|
val editor = remember(option.type, option.presets) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val baseOptionEditor =
|
val baseOptionEditor =
|
||||||
optionEditors.getOrDefault(option.type, UnknownTypeEditor) as OptionEditor<T>
|
optionEditors.getOrDefault(option.type, UnknownTypeEditor) as OptionEditor<T>
|
||||||
|
|
||||||
if (option.type != "Boolean" && option.presets != null) PresetOptionEditor(baseOptionEditor)
|
if (option.type != typeOf<Boolean>() && option.presets != null) PresetOptionEditor(
|
||||||
|
baseOptionEditor
|
||||||
|
)
|
||||||
else baseOptionEditor
|
else baseOptionEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,7 +162,15 @@ fun <T : Any> OptionItem(option: Option<T>, value: T?, setValue: (T?) -> Unit) {
|
|||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable(onClick = ::clickAction),
|
modifier = Modifier.clickable(onClick = ::clickAction),
|
||||||
headlineContent = { Text(option.title) },
|
headlineContent = { Text(option.title) },
|
||||||
supportingContent = { Text(option.description) },
|
supportingContent = {
|
||||||
|
Column {
|
||||||
|
Text(option.description)
|
||||||
|
if (option.required && value == null) Text(
|
||||||
|
stringResource(R.string.option_required),
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
trailingContent = { ListItemTrailingContent() }
|
trailingContent = { ListItemTrailingContent() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -335,7 +331,7 @@ private object BooleanOptionEditor : OptionEditor<Boolean> {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ListItemTrailingContent(scope: OptionEditorScope<Boolean>) {
|
override fun ListItemTrailingContent(scope: OptionEditorScope<Boolean>) {
|
||||||
Switch(checked = scope.current, onCheckedChange = scope.setValue)
|
HapticSwitch(checked = scope.current, onCheckedChange = scope.setValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -422,11 +418,12 @@ private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<
|
|||||||
headlineContent = { Text(title) },
|
headlineContent = { Text(title) },
|
||||||
supportingContent = value?.toString()?.let { { Text(it) } },
|
supportingContent = value?.toString()?.let { { Text(it) } },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
RadioButton(
|
HapticRadioButton(
|
||||||
selected = selectedPreset == presetKey,
|
selected = selectedPreset == presetKey,
|
||||||
onClick = { selectedPreset = presetKey }
|
onClick = { selectedPreset = presetKey }
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
colors = transparentListItemColors
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,7 +448,7 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
|||||||
option.key,
|
option.key,
|
||||||
option.description,
|
option.description,
|
||||||
option.required,
|
option.required,
|
||||||
option.type.removeSuffix("Array"),
|
option.type.arguments.first().type!!,
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
) { true }
|
) { true }
|
||||||
@ -568,7 +565,7 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
|||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (deleteMode) return@Scaffold
|
if (deleteMode) return@Scaffold
|
||||||
|
|
||||||
ExtendedFloatingActionButton(
|
HapticExtendedFloatingActionButton(
|
||||||
text = { Text(stringResource(R.string.add)) },
|
text = { Text(stringResource(R.string.add)) },
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -2,13 +2,13 @@ package app.revanced.manager.ui.component.settings
|
|||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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.domain.manager.base.Preference
|
import app.revanced.manager.domain.manager.base.Preference
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ fun BooleanItem(
|
|||||||
headlineContent = stringResource(headline),
|
headlineContent = stringResource(headline),
|
||||||
supportingContent = stringResource(description),
|
supportingContent = stringResource(description),
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
HapticSwitch(
|
||||||
checked = value,
|
checked = value,
|
||||||
onCheckedChange = onValueChange,
|
onCheckedChange = onValueChange,
|
||||||
)
|
)
|
||||||
|
@ -26,7 +26,6 @@ import app.revanced.manager.ui.component.Markdown
|
|||||||
fun Changelog(
|
fun Changelog(
|
||||||
markdown: String,
|
markdown: String,
|
||||||
version: String,
|
version: String,
|
||||||
downloadCount: String,
|
|
||||||
publishDate: String
|
publishDate: String
|
||||||
) {
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
@ -55,10 +54,6 @@ fun Changelog(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Tag(
|
|
||||||
Icons.Outlined.FileDownload,
|
|
||||||
downloadCount
|
|
||||||
)
|
|
||||||
Tag(
|
Tag(
|
||||||
Icons.Outlined.CalendarToday,
|
Icons.Outlined.CalendarToday,
|
||||||
publishDate
|
publishDate
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
package app.revanced.manager.ui.component.settings
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.revanced.manager.domain.manager.base.Preference
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SafeguardBooleanItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
preference: Preference<Boolean>,
|
||||||
|
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
||||||
|
@StringRes headline: Int,
|
||||||
|
@StringRes description: Int,
|
||||||
|
@StringRes confirmationText: Int
|
||||||
|
) {
|
||||||
|
val value by preference.getAsState()
|
||||||
|
var showSafeguardWarning by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSafeguardWarning) {
|
||||||
|
SafeguardConfirmationDialog(
|
||||||
|
onDismiss = { showSafeguardWarning = false },
|
||||||
|
onConfirm = {
|
||||||
|
coroutineScope.launch { preference.update(!value) }
|
||||||
|
showSafeguardWarning = false
|
||||||
|
},
|
||||||
|
body = stringResource(confirmationText)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BooleanItem(
|
||||||
|
modifier = modifier,
|
||||||
|
value = value,
|
||||||
|
onValueChange = {
|
||||||
|
if (it != preference.default) {
|
||||||
|
showSafeguardWarning = true
|
||||||
|
} else {
|
||||||
|
coroutineScope.launch { preference.update(it) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headline = headline,
|
||||||
|
description = description
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package app.revanced.manager.ui.component.settings
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.WarningAmber
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import app.revanced.manager.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SafeguardConfirmationDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
body: String,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onConfirm) {
|
||||||
|
Text(stringResource(R.string.yes))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.no))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(Icons.Outlined.WarningAmber, null)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.warning),
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(body)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
@ -22,13 +22,36 @@ fun SettingsListItem(
|
|||||||
colors: ListItemColors = ListItemDefaults.colors(),
|
colors: ListItemColors = ListItemDefaults.colors(),
|
||||||
tonalElevation: Dp = ListItemDefaults.Elevation,
|
tonalElevation: Dp = ListItemDefaults.Elevation,
|
||||||
shadowElevation: Dp = ListItemDefaults.Elevation,
|
shadowElevation: Dp = ListItemDefaults.Elevation,
|
||||||
) = ListItem(
|
) = SettingsListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
text = headlineContent,
|
text = headlineContent,
|
||||||
style = MaterialTheme.typography.titleLarge
|
style = MaterialTheme.typography.titleLarge
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
overlineContent = overlineContent,
|
||||||
|
supportingContent = supportingContent,
|
||||||
|
leadingContent = leadingContent,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
colors = colors,
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
shadowElevation = shadowElevation
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsListItem(
|
||||||
|
headlineContent: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
overlineContent: @Composable (() -> Unit)? = null,
|
||||||
|
supportingContent: String? = null,
|
||||||
|
leadingContent: @Composable (() -> Unit)? = null,
|
||||||
|
trailingContent: @Composable (() -> Unit)? = null,
|
||||||
|
colors: ListItemColors = ListItemDefaults.colors(),
|
||||||
|
tonalElevation: Dp = ListItemDefaults.Elevation,
|
||||||
|
shadowElevation: Dp = ListItemDefaults.Elevation,
|
||||||
|
) = ListItem(
|
||||||
|
headlineContent = headlineContent,
|
||||||
modifier = modifier.then(Modifier.padding(horizontal = 8.dp)),
|
modifier = modifier.then(Modifier.padding(horizontal = 8.dp)),
|
||||||
overlineContent = overlineContent,
|
overlineContent = overlineContent,
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
package app.revanced.manager.ui.destination
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
|
||||||
import app.revanced.manager.util.Options
|
|
||||||
import app.revanced.manager.util.PatchSelection
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import kotlinx.parcelize.RawValue
|
|
||||||
|
|
||||||
sealed interface Destination : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object Dashboard : Destination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object AppSelector : Destination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Settings(val startDestination: SettingsDestination = SettingsDestination.Settings) : Destination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class VersionSelector(val packageName: String, val patchSelection: PatchSelection? = null) : Destination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchSelection: PatchSelection? = null) : Destination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Patcher(val selectedApp: SelectedApp, val selectedPatches: PatchSelection, val options: @RawValue Options) : Destination
|
|
||||||
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package app.revanced.manager.ui.destination
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
|
||||||
import app.revanced.manager.util.Options
|
|
||||||
import app.revanced.manager.util.PatchSelection
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import kotlinx.parcelize.RawValue
|
|
||||||
|
|
||||||
sealed interface SelectedAppInfoDestination : Parcelable {
|
|
||||||
@Parcelize
|
|
||||||
data object Main : SelectedAppInfoDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class PatchesSelector(val app: SelectedApp, val currentSelection: PatchSelection?, val options: @RawValue Options) : SelectedAppInfoDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object VersionSelector: SelectedAppInfoDestination
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
package app.revanced.manager.ui.destination
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
sealed interface SettingsDestination : Parcelable {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object Settings : SettingsDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object General : SettingsDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object Advanced : SettingsDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object Updates : SettingsDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object Downloads : SettingsDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object ImportExport : SettingsDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object About : SettingsDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Update(val downloadOnScreenEntry: Boolean = false) : SettingsDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object Changelogs : SettingsDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object Contributors: SettingsDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object Licenses: SettingsDestination
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object DeveloperOptions: SettingsDestination
|
|
||||||
}
|
|
@ -34,22 +34,25 @@ data class BundleInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object Extensions {
|
companion object Extensions {
|
||||||
inline fun Iterable<BundleInfo>.toPatchSelection(allowUnsupported: Boolean, condition: (Int, PatchInfo) -> Boolean): PatchSelection = this.associate { bundle ->
|
inline fun Iterable<BundleInfo>.toPatchSelection(
|
||||||
val patches =
|
allowUnsupported: Boolean,
|
||||||
bundle.patchSequence(allowUnsupported)
|
condition: (Int, PatchInfo) -> Boolean
|
||||||
.mapNotNullTo(mutableSetOf()) { patch ->
|
): PatchSelection = this.associate { bundle ->
|
||||||
patch.name.takeIf {
|
val patches =
|
||||||
condition(
|
bundle.patchSequence(allowUnsupported)
|
||||||
bundle.uid,
|
.mapNotNullTo(mutableSetOf()) { patch ->
|
||||||
patch
|
patch.name.takeIf {
|
||||||
)
|
condition(
|
||||||
}
|
bundle.uid,
|
||||||
|
patch
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bundle.uid to patches
|
bundle.uid to patches
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String) =
|
fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String?) =
|
||||||
sources.flatMapLatestAndCombine(
|
sources.flatMapLatestAndCombine(
|
||||||
combiner = { it.filterNotNull() }
|
combiner = { it.filterNotNull() }
|
||||||
) { source ->
|
) { source ->
|
||||||
@ -64,7 +67,7 @@ data class BundleInfo(
|
|||||||
bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
|
bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
|
||||||
val targetList = when {
|
val targetList = when {
|
||||||
it.compatiblePackages == null -> universal
|
it.compatiblePackages == null -> universal
|
||||||
it.supportsVersion(
|
it.supports(
|
||||||
packageName,
|
packageName,
|
||||||
version
|
version
|
||||||
) -> supported
|
) -> supported
|
||||||
@ -78,6 +81,28 @@ data class BundleInfo(
|
|||||||
BundleInfo(source.getName(), source.uid, supported, unsupported, universal)
|
BundleInfo(source.getName(), source.uid, supported, unsupported, universal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Algorithm for determining whether all required options have been set.
|
||||||
|
*/
|
||||||
|
inline fun Iterable<BundleInfo>.requiredOptionsSet(
|
||||||
|
crossinline isSelected: (BundleInfo, PatchInfo) -> Boolean,
|
||||||
|
crossinline optionsForPatch: (BundleInfo, PatchInfo) -> Map<String, Any?>?
|
||||||
|
) = all bundle@{ bundle ->
|
||||||
|
bundle
|
||||||
|
.all
|
||||||
|
.filter { isSelected(bundle, it) }
|
||||||
|
.all patch@{
|
||||||
|
if (it.options.isNullOrEmpty()) return@patch true
|
||||||
|
val opts by lazy { optionsForPatch(bundle, it).orEmpty() }
|
||||||
|
|
||||||
|
it.options.all option@{ option ->
|
||||||
|
if (!option.required || option.default != null) return@option true
|
||||||
|
|
||||||
|
option.key in opts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
package app.revanced.manager.ui.model
|
||||||
|
|
||||||
|
interface InstallerModel {
|
||||||
|
fun reinstall()
|
||||||
|
fun install()
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
package app.revanced.manager.ui.model
|
package app.revanced.manager.ui.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
enum class StepCategory(@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),
|
||||||
@ -14,10 +15,19 @@ enum class State {
|
|||||||
WAITING, RUNNING, FAILED, COMPLETED
|
WAITING, RUNNING, FAILED, COMPLETED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class ProgressKey {
|
||||||
|
DOWNLOAD
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StepProgressProvider {
|
||||||
|
val downloadProgress: Pair<Long, Long?>?
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
data class Step(
|
data class Step(
|
||||||
val name: String,
|
val name: String,
|
||||||
val category: StepCategory,
|
val category: StepCategory,
|
||||||
val state: State = State.WAITING,
|
val state: State = State.WAITING,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val downloadProgress: StateFlow<Pair<Float, Float>?>? = null
|
val progressKey: ProgressKey? = null
|
||||||
)
|
) : Parcelable
|
@ -1,20 +1,35 @@
|
|||||||
package app.revanced.manager.ui.model
|
package app.revanced.manager.ui.model
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import app.revanced.manager.network.downloader.AppDownloader
|
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
sealed class SelectedApp : Parcelable {
|
sealed interface SelectedApp : Parcelable {
|
||||||
abstract val packageName: String
|
val packageName: String
|
||||||
abstract val version: String
|
val version: String?
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Download(override val packageName: String, override val version: String, val app: AppDownloader.App) : SelectedApp()
|
data class Download(
|
||||||
|
override val packageName: String,
|
||||||
|
override val version: String?,
|
||||||
|
val data: ParceledDownloaderData
|
||||||
|
) : SelectedApp
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Local(override val packageName: String, override val version: String, val file: File, val temporary: Boolean) : SelectedApp()
|
data class Search(override val packageName: String, override val version: String?) : SelectedApp
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Installed(override val packageName: String, override val version: String) : SelectedApp()
|
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
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
package app.revanced.manager.ui.model.navigation
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
|
import app.revanced.manager.util.Options
|
||||||
|
import app.revanced.manager.util.PatchSelection
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.parcelize.RawValue
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
interface ComplexParameter<T : Parcelable>
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object Dashboard
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object AppSelector
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class InstalledApplicationInfo(val packageName: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Update(val downloadOnScreenEntry: Boolean = false)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> {
|
||||||
|
@Parcelize
|
||||||
|
data class ViewModelParams(
|
||||||
|
val app: SelectedApp,
|
||||||
|
val patches: PatchSelection? = null
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object Main
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
|
||||||
|
@Parcelize
|
||||||
|
data class ViewModelParams(
|
||||||
|
val app: SelectedApp,
|
||||||
|
val currentSelection: PatchSelection?,
|
||||||
|
val options: @RawValue Options,
|
||||||
|
) : Parcelable
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
|
||||||
|
@Parcelize
|
||||||
|
data class ViewModelParams(
|
||||||
|
val selectedApp: SelectedApp,
|
||||||
|
val selectedPatches: PatchSelection,
|
||||||
|
val options: @RawValue Options
|
||||||
|
) : Parcelable
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object Settings {
|
||||||
|
sealed interface Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Main : Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object General : Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Advanced : Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Updates : Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Downloads : Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object ImportExport : Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object About : Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Changelogs : Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Contributors : Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Licenses : Destination
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object DeveloperOptions : Destination
|
||||||
|
}
|
@ -10,7 +10,6 @@ import androidx.compose.material.icons.filled.Storage
|
|||||||
import androidx.compose.material.icons.outlined.Search
|
import androidx.compose.material.icons.outlined.Search
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
|
||||||
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.remember
|
||||||
@ -33,18 +32,20 @@ import app.revanced.manager.ui.component.SearchView
|
|||||||
import app.revanced.manager.ui.model.SelectedApp
|
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.transparentListItemColors
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppSelectorScreen(
|
fun AppSelectorScreen(
|
||||||
onAppClick: (packageName: String) -> Unit,
|
onSelect: (String) -> Unit,
|
||||||
onStorageClick: (SelectedApp.Local) -> Unit,
|
onStorageSelect: (SelectedApp.Local) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: AppSelectorViewModel = koinViewModel()
|
vm: AppSelectorViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
SideEffect {
|
EventEffect(flow = vm.storageSelectionFlow) {
|
||||||
vm.onStorageClick = onStorageClick
|
onStorageSelect(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val pickApkLauncher =
|
val pickApkLauncher =
|
||||||
@ -74,7 +75,7 @@ fun AppSelectorScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search)
|
||||||
SearchView(
|
SearchView(
|
||||||
query = filterText,
|
query = filterText,
|
||||||
onQueryChange = { filterText = it },
|
onQueryChange = { filterText = it },
|
||||||
@ -82,15 +83,15 @@ fun AppSelectorScreen(
|
|||||||
placeholder = { Text(stringResource(R.string.search_apps)) }
|
placeholder = { Text(stringResource(R.string.search_apps)) }
|
||||||
) {
|
) {
|
||||||
if (appList.isNotEmpty() && filterText.isNotEmpty()) {
|
if (appList.isNotEmpty() && filterText.isNotEmpty()) {
|
||||||
LazyColumnWithScrollbar(
|
LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) {
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
items(
|
items(
|
||||||
items = filteredAppList,
|
items = filteredAppList,
|
||||||
key = { it.packageName }
|
key = { it.packageName }
|
||||||
) { app ->
|
) { app ->
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { onAppClick(app.packageName) },
|
modifier = Modifier.clickable {
|
||||||
|
onSelect(app.packageName)
|
||||||
|
},
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
AppIcon(
|
AppIcon(
|
||||||
app.packageInfo,
|
app.packageInfo,
|
||||||
@ -110,9 +111,9 @@ fun AppSelectorScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
colors = transparentListItemColors
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -124,17 +125,18 @@ fun AppSelectorScreen(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Search,
|
imageVector = Icons.Outlined.Search,
|
||||||
contentDescription = stringResource(R.string.search),
|
contentDescription = stringResource(R.string.search),
|
||||||
modifier = Modifier.size(64.dp)
|
modifier = Modifier.size(64.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.type_anything),
|
text = stringResource(R.string.type_anything),
|
||||||
style = MaterialTheme.typography.bodyLarge
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -183,7 +185,9 @@ fun AppSelectorScreen(
|
|||||||
key = { it.packageName }
|
key = { it.packageName }
|
||||||
) { app ->
|
) { app ->
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { onAppClick(app.packageName) },
|
modifier = Modifier.clickable {
|
||||||
|
onSelect(app.packageName)
|
||||||
|
},
|
||||||
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
|
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
AppLabel(
|
AppLabel(
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||||
|
import app.revanced.manager.ui.component.bundle.BundleItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BundleListScreen(
|
||||||
|
onDelete: (PatchBundleSource) -> Unit,
|
||||||
|
onUpdate: (PatchBundleSource) -> Unit,
|
||||||
|
sources: List<PatchBundleSource>,
|
||||||
|
selectedSources: SnapshotStateList<PatchBundleSource>,
|
||||||
|
bundlesSelectable: Boolean,
|
||||||
|
) {
|
||||||
|
LazyColumnWithScrollbar(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
sources,
|
||||||
|
key = { it.uid }
|
||||||
|
) { source ->
|
||||||
|
BundleItem(
|
||||||
|
bundle = source,
|
||||||
|
onDelete = {
|
||||||
|
onDelete(source)
|
||||||
|
},
|
||||||
|
onUpdate = {
|
||||||
|
onUpdate(source)
|
||||||
|
},
|
||||||
|
selectable = bundlesSelectable,
|
||||||
|
onSelect = {
|
||||||
|
selectedSources.add(source)
|
||||||
|
},
|
||||||
|
isBundleSelected = selectedSources.contains(source),
|
||||||
|
toggleSelection = { bundleIsNotSelected ->
|
||||||
|
if (bundleIsNotSelected) {
|
||||||
|
selectedSources.add(source)
|
||||||
|
} else {
|
||||||
|
selectedSources.remove(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,8 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
@ -24,17 +25,19 @@ 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.room.apps.installed.InstalledApp
|
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||||
import app.revanced.manager.patcher.aapt.Aapt
|
import app.revanced.manager.patcher.aapt.Aapt
|
||||||
|
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||||
import app.revanced.manager.ui.component.AvailableUpdateDialog
|
import app.revanced.manager.ui.component.AvailableUpdateDialog
|
||||||
import app.revanced.manager.ui.component.NotificationCard
|
import app.revanced.manager.ui.component.NotificationCard
|
||||||
import app.revanced.manager.ui.component.bundle.BundleItem
|
|
||||||
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||||
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
|
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
|
||||||
import app.revanced.manager.ui.viewmodel.DashboardViewModel
|
import app.revanced.manager.ui.viewmodel.DashboardViewModel
|
||||||
|
import app.revanced.manager.util.RequestInstallAppsContract
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
@ -48,17 +51,21 @@ enum class DashboardPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("BatteryLife")
|
@SuppressLint("BatteryLife")
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardScreen(
|
fun DashboardScreen(
|
||||||
vm: DashboardViewModel = koinViewModel(),
|
vm: DashboardViewModel = koinViewModel(),
|
||||||
onAppSelectorClick: () -> Unit,
|
onAppSelectorClick: () -> Unit,
|
||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit,
|
||||||
onUpdateClick: () -> Unit,
|
onUpdateClick: () -> Unit,
|
||||||
onAppClick: (InstalledApp) -> Unit
|
onDownloaderPluginClick: () -> Unit,
|
||||||
|
onAppClick: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
|
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
|
||||||
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
||||||
|
val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
|
||||||
|
false
|
||||||
|
)
|
||||||
val androidContext = LocalContext.current
|
val androidContext = LocalContext.current
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState(
|
val pagerState = rememberPagerState(
|
||||||
@ -77,9 +84,9 @@ fun DashboardScreen(
|
|||||||
if (showAddBundleDialog) {
|
if (showAddBundleDialog) {
|
||||||
ImportPatchBundleDialog(
|
ImportPatchBundleDialog(
|
||||||
onDismiss = { showAddBundleDialog = false },
|
onDismiss = { showAddBundleDialog = false },
|
||||||
onLocalSubmit = { patches, integrations ->
|
onLocalSubmit = { patches ->
|
||||||
showAddBundleDialog = false
|
showAddBundleDialog = false
|
||||||
vm.createLocalSource(patches, integrations)
|
vm.createLocalSource(patches)
|
||||||
},
|
},
|
||||||
onRemoteSubmit = { url, autoUpdate ->
|
onRemoteSubmit = { url, autoUpdate ->
|
||||||
showAddBundleDialog = false
|
showAddBundleDialog = false
|
||||||
@ -88,20 +95,35 @@ fun DashboardScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var showDialog by rememberSaveable { mutableStateOf(vm.prefs.showManagerUpdateDialogOnLaunch.getBlocking()) }
|
var showUpdateDialog by rememberSaveable { mutableStateOf(vm.prefs.showManagerUpdateDialogOnLaunch.getBlocking()) }
|
||||||
val availableUpdate by remember {
|
val availableUpdate by remember {
|
||||||
derivedStateOf { vm.updatedManagerVersion.takeIf { showDialog } }
|
derivedStateOf { vm.updatedManagerVersion.takeIf { showUpdateDialog } }
|
||||||
}
|
}
|
||||||
|
|
||||||
availableUpdate?.let { version ->
|
availableUpdate?.let { version ->
|
||||||
AvailableUpdateDialog(
|
AvailableUpdateDialog(
|
||||||
onDismiss = { showDialog = false },
|
onDismiss = { showUpdateDialog = false },
|
||||||
setShowManagerUpdateDialogOnLaunch = vm::setShowManagerUpdateDialogOnLaunch,
|
setShowManagerUpdateDialogOnLaunch = vm::setShowManagerUpdateDialogOnLaunch,
|
||||||
onConfirm = onUpdateClick,
|
onConfirm = onUpdateClick,
|
||||||
newVersion = version
|
newVersion = version
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showAndroid11Dialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val installAppsPermissionLauncher =
|
||||||
|
rememberLauncherForActivityResult(RequestInstallAppsContract) { granted ->
|
||||||
|
showAndroid11Dialog = false
|
||||||
|
if (granted) onAppSelectorClick()
|
||||||
|
}
|
||||||
|
if (showAndroid11Dialog) Android11Dialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
showAndroid11Dialog = false
|
||||||
|
},
|
||||||
|
onContinue = {
|
||||||
|
installAppsPermissionLauncher.launch(androidContext.packageName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
if (bundlesSelectable) {
|
if (bundlesSelectable) {
|
||||||
@ -168,7 +190,7 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(
|
HapticFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
vm.cancelSourceSelection()
|
vm.cancelSourceSelection()
|
||||||
|
|
||||||
@ -181,7 +203,11 @@ fun DashboardScreen(
|
|||||||
DashboardPage.BUNDLES.ordinal
|
DashboardPage.BUNDLES.ordinal
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@FloatingActionButton
|
return@HapticFloatingActionButton
|
||||||
|
}
|
||||||
|
if (vm.android11BugActive) {
|
||||||
|
showAndroid11Dialog = true
|
||||||
|
return@HapticFloatingActionButton
|
||||||
}
|
}
|
||||||
|
|
||||||
onAppSelectorClick()
|
onAppSelectorClick()
|
||||||
@ -201,7 +227,7 @@ fun DashboardScreen(
|
|||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
) {
|
) {
|
||||||
DashboardPage.entries.forEachIndexed { index, page ->
|
DashboardPage.entries.forEachIndexed { index, page ->
|
||||||
Tab(
|
HapticTab(
|
||||||
selected = pagerState.currentPage == index,
|
selected = pagerState.currentPage == index,
|
||||||
onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } },
|
onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } },
|
||||||
text = { Text(stringResource(page.titleResId)) },
|
text = { Text(stringResource(page.titleResId)) },
|
||||||
@ -212,6 +238,7 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val showBatteryOptimizationsWarning by vm.showBatteryOptimizationsWarningFlow.collectAsStateWithLifecycle(false)
|
||||||
Notifications(
|
Notifications(
|
||||||
if (!Aapt.supportsDevice()) {
|
if (!Aapt.supportsDevice()) {
|
||||||
{
|
{
|
||||||
@ -223,7 +250,7 @@ fun DashboardScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
if (vm.showBatteryOptimizationsWarning) {
|
if (showBatteryOptimizationsWarning) {
|
||||||
{
|
{
|
||||||
NotificationCard(
|
NotificationCard(
|
||||||
isWarning = true,
|
isWarning = true,
|
||||||
@ -236,6 +263,20 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else null,
|
||||||
|
if (showNewDownloaderPluginsNotification) {
|
||||||
|
{
|
||||||
|
NotificationCard(
|
||||||
|
text = stringResource(R.string.new_downloader_plugins_notification),
|
||||||
|
icon = Icons.Outlined.Download,
|
||||||
|
modifier = Modifier.clickable(onClick = onDownloaderPluginClick),
|
||||||
|
actions = {
|
||||||
|
TextButton(onClick = vm::ignoreNewDownloaderPlugins) {
|
||||||
|
Text(stringResource(R.string.dismiss))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
} else null
|
} else null
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -247,7 +288,7 @@ fun DashboardScreen(
|
|||||||
when (DashboardPage.entries[index]) {
|
when (DashboardPage.entries[index]) {
|
||||||
DashboardPage.DASHBOARD -> {
|
DashboardPage.DASHBOARD -> {
|
||||||
InstalledAppsScreen(
|
InstalledAppsScreen(
|
||||||
onAppClick = onAppClick
|
onAppClick = { onAppClick(it.currentPackageName) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,33 +303,17 @@ fun DashboardScreen(
|
|||||||
|
|
||||||
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
|
||||||
Column(
|
BundleListScreen(
|
||||||
modifier = Modifier.fillMaxSize(),
|
onDelete = {
|
||||||
) {
|
vm.delete(it)
|
||||||
sources.forEach {
|
},
|
||||||
BundleItem(
|
onUpdate = {
|
||||||
bundle = it,
|
vm.update(it)
|
||||||
onDelete = {
|
},
|
||||||
vm.delete(it)
|
sources = sources,
|
||||||
},
|
selectedSources = vm.selectedSources,
|
||||||
onUpdate = {
|
bundlesSelectable = bundlesSelectable
|
||||||
vm.update(it)
|
)
|
||||||
},
|
|
||||||
selectable = bundlesSelectable,
|
|
||||||
onSelect = {
|
|
||||||
vm.selectedSources.add(it)
|
|
||||||
},
|
|
||||||
isBundleSelected = vm.selectedSources.contains(it),
|
|
||||||
toggleSelection = { bundleIsNotSelected ->
|
|
||||||
if (bundleIsNotSelected) {
|
|
||||||
vm.selectedSources.add(it)
|
|
||||||
} else {
|
|
||||||
vm.selectedSources.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -314,3 +339,24 @@ fun Notifications(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Android11Dialog(onDismissRequest: () -> Unit, onContinue: () -> Unit) {
|
||||||
|
AlertDialogExtended(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onContinue) {
|
||||||
|
Text(stringResource(R.string.continue_))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(stringResource(R.string.android_11_bug_dialog_title))
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(Icons.Outlined.BugReport, null)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.android_11_bug_dialog_description))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
@ -41,13 +41,12 @@ import app.revanced.manager.ui.component.ColumnWithScrollbar
|
|||||||
import app.revanced.manager.ui.component.SegmentedButton
|
import app.revanced.manager.ui.component.SegmentedButton
|
||||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.viewmodel.InstalledAppInfoViewModel
|
import app.revanced.manager.ui.viewmodel.InstalledAppInfoViewModel
|
||||||
import app.revanced.manager.util.PatchSelection
|
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun InstalledAppInfoScreen(
|
fun InstalledAppInfoScreen(
|
||||||
onPatchClick: (packageName: String, patchSelection: PatchSelection) -> Unit,
|
onPatchClick: (packageName: String) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
viewModel: InstalledAppInfoViewModel
|
viewModel: InstalledAppInfoViewModel
|
||||||
) {
|
) {
|
||||||
@ -78,10 +77,12 @@ fun InstalledAppInfoScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
AppInfo(viewModel.appInfo) {
|
val installedApp = viewModel.installedApp ?: return@ColumnWithScrollbar
|
||||||
Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
|
|
||||||
|
|
||||||
if (viewModel.installedApp.installType == InstallType.MOUNT) {
|
AppInfo(viewModel.appInfo) {
|
||||||
|
Text(installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
|
||||||
|
if (installedApp.installType == InstallType.MOUNT) {
|
||||||
Text(
|
Text(
|
||||||
text = if (viewModel.isMounted) {
|
text = if (viewModel.isMounted) {
|
||||||
stringResource(R.string.mounted)
|
stringResource(R.string.mounted)
|
||||||
@ -105,7 +106,7 @@ fun InstalledAppInfoScreen(
|
|||||||
onClick = viewModel::launch
|
onClick = viewModel::launch
|
||||||
)
|
)
|
||||||
|
|
||||||
when (viewModel.installedApp.installType) {
|
when (installedApp.installType) {
|
||||||
InstallType.DEFAULT -> SegmentedButton(
|
InstallType.DEFAULT -> SegmentedButton(
|
||||||
icon = Icons.Outlined.Delete,
|
icon = Icons.Outlined.Delete,
|
||||||
text = stringResource(R.string.uninstall),
|
text = stringResource(R.string.uninstall),
|
||||||
@ -134,11 +135,9 @@ fun InstalledAppInfoScreen(
|
|||||||
icon = Icons.Outlined.Update,
|
icon = Icons.Outlined.Update,
|
||||||
text = stringResource(R.string.repatch),
|
text = stringResource(R.string.repatch),
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.appliedPatches?.let {
|
onPatchClick(installedApp.originalPackageName)
|
||||||
onPatchClick(viewModel.installedApp.originalPackageName, it)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
enabled = viewModel.installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess()
|
enabled = installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,19 +160,19 @@ fun InstalledAppInfoScreen(
|
|||||||
|
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
headlineContent = stringResource(R.string.package_name),
|
headlineContent = stringResource(R.string.package_name),
|
||||||
supportingContent = viewModel.installedApp.currentPackageName
|
supportingContent = installedApp.currentPackageName
|
||||||
)
|
)
|
||||||
|
|
||||||
if (viewModel.installedApp.originalPackageName != viewModel.installedApp.currentPackageName) {
|
if (installedApp.originalPackageName != installedApp.currentPackageName) {
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
headlineContent = stringResource(R.string.original_package_name),
|
headlineContent = stringResource(R.string.original_package_name),
|
||||||
supportingContent = viewModel.installedApp.originalPackageName
|
supportingContent = installedApp.originalPackageName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
headlineContent = stringResource(R.string.install_type),
|
headlineContent = stringResource(R.string.install_type),
|
||||||
supportingContent = stringResource(viewModel.installedApp.installType.stringResource)
|
supportingContent = stringResource(installedApp.installType.stringResource)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@ -17,14 +15,9 @@ import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
|||||||
import androidx.compose.material.icons.outlined.FileDownload
|
import androidx.compose.material.icons.outlined.FileDownload
|
||||||
import androidx.compose.material.icons.outlined.PostAdd
|
import androidx.compose.material.icons.outlined.PostAdd
|
||||||
import androidx.compose.material.icons.outlined.Save
|
import androidx.compose.material.icons.outlined.Save
|
||||||
import androidx.compose.material3.BottomAppBar
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
@ -36,18 +29,18 @@ 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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
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.InstallerStatusDialog
|
import app.revanced.manager.ui.component.InstallerStatusDialog
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
|
import app.revanced.manager.ui.component.patcher.InstallPickerDialog
|
||||||
import app.revanced.manager.ui.component.patcher.Steps
|
import app.revanced.manager.ui.component.patcher.Steps
|
||||||
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.viewmodel.PatcherViewModel
|
import app.revanced.manager.ui.viewmodel.PatcherViewModel
|
||||||
import app.revanced.manager.util.APK_MIMETYPE
|
import app.revanced.manager.util.APK_MIMETYPE
|
||||||
|
import app.revanced.manager.util.EventEffect
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -55,7 +48,11 @@ fun PatcherScreen(
|
|||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: PatcherViewModel
|
vm: PatcherViewModel
|
||||||
) {
|
) {
|
||||||
BackHandler(onBack = onBackClick)
|
fun leaveScreen() {
|
||||||
|
vm.onBack()
|
||||||
|
onBackClick()
|
||||||
|
}
|
||||||
|
BackHandler(onBack = ::leaveScreen)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val exportApkLauncher =
|
val exportApkLauncher =
|
||||||
@ -71,19 +68,13 @@ fun PatcherScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val patchesProgress by vm.patchesProgress.collectAsStateWithLifecycle()
|
if (patcherSucceeded == null) {
|
||||||
|
DisposableEffect(Unit) {
|
||||||
val progress by remember {
|
val window = (context as Activity).window
|
||||||
derivedStateOf {
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
val (patchesCompleted, patchesTotal) = patchesProgress
|
onDispose {
|
||||||
|
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
val current = vm.steps.count {
|
}
|
||||||
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
|
|
||||||
} + patchesCompleted
|
|
||||||
|
|
||||||
val total = vm.steps.size - 1 + patchesTotal
|
|
||||||
|
|
||||||
current.toFloat() / total.toFloat()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,14 +84,47 @@ fun PatcherScreen(
|
|||||||
onConfirm = vm::install
|
onConfirm = vm::install
|
||||||
)
|
)
|
||||||
|
|
||||||
if (vm.installerStatusDialogModel.packageInstallerStatus != null)
|
vm.packageInstallerStatus?.let {
|
||||||
InstallerStatusDialog(vm.installerStatusDialogModel)
|
InstallerStatusDialog(it, vm, vm::dismissPackageInstallerDialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
val activityLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult(),
|
||||||
|
onResult = vm::handleActivityResult
|
||||||
|
)
|
||||||
|
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
||||||
|
activityLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.activityPromptDialog?.let { title ->
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = vm::rejectInteraction,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = vm::allowInteraction
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.continue_))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = vm::rejectInteraction
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text(title) },
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.plugin_activity_dialog_body))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
title = stringResource(R.string.patcher),
|
title = stringResource(R.string.patcher),
|
||||||
onBackClick = onBackClick
|
onBackClick = ::leaveScreen
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
@ -121,7 +145,7 @@ fun PatcherScreen(
|
|||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(visible = canInstall) {
|
AnimatedVisibility(visible = canInstall) {
|
||||||
ExtendedFloatingActionButton(
|
HapticExtendedFloatingActionButton(
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(if (vm.installedPackageName == null) R.string.install_app else R.string.open_app)
|
stringResource(if (vm.installedPackageName == null) R.string.install_app else R.string.open_app)
|
||||||
@ -156,7 +180,7 @@ fun PatcherScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
progress = { progress },
|
progress = { vm.progress },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -172,7 +196,8 @@ fun PatcherScreen(
|
|||||||
Steps(
|
Steps(
|
||||||
category = category,
|
category = category,
|
||||||
steps = steps,
|
steps = steps,
|
||||||
stepCount = if (category == StepCategory.PATCHING) patchesProgress else null
|
stepCount = if (category == StepCategory.PATCHING) vm.patchesProgress else null,
|
||||||
|
stepProgressProvider = vm
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,50 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.EaseInOut
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||||
import androidx.compose.material.icons.outlined.*
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material.icons.outlined.FilterList
|
||||||
|
import androidx.compose.material.icons.outlined.Restore
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
import androidx.compose.material.icons.outlined.WarningAmber
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.ScrollableTabRow
|
||||||
|
import androidx.compose.material3.SmallFloatingActionButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
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
|
||||||
@ -20,8 +53,10 @@ 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.ui.Alignment
|
||||||
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.draw.rotate
|
||||||
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
|
||||||
@ -32,20 +67,24 @@ import app.revanced.manager.R
|
|||||||
import app.revanced.manager.patcher.patch.Option
|
import app.revanced.manager.patcher.patch.Option
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
|
import app.revanced.manager.ui.component.CheckedFilterChip
|
||||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.SafeguardDialog
|
import app.revanced.manager.ui.component.SafeguardDialog
|
||||||
import app.revanced.manager.ui.component.SearchView
|
import app.revanced.manager.ui.component.SearchBar
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||||
import app.revanced.manager.ui.component.patches.OptionItem
|
import app.revanced.manager.ui.component.patches.OptionItem
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED
|
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
||||||
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.isScrollingUp
|
import app.revanced.manager.util.isScrollingUp
|
||||||
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PatchesSelectorScreen(
|
fun PatchesSelectorScreen(
|
||||||
onSave: (PatchSelection?, Options) -> Unit,
|
onSave: (PatchSelection?, Options) -> Unit,
|
||||||
@ -60,20 +99,17 @@ fun PatchesSelectorScreen(
|
|||||||
bundles.size
|
bundles.size
|
||||||
}
|
}
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
var search: String? by rememberSaveable {
|
val (query, setQuery) = rememberSaveable {
|
||||||
mutableStateOf(null)
|
mutableStateOf("")
|
||||||
|
}
|
||||||
|
val (searchExpanded, setSearchExpanded) = rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
|
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
|
||||||
val showPatchButton by remember {
|
val showSaveButton by remember {
|
||||||
derivedStateOf { vm.selectionIsValid(bundles) }
|
derivedStateOf { vm.selectionIsValid(bundles) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val availablePatchCount by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
bundles.sumOf { it.patchCount }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val defaultPatchSelectionCount by vm.defaultSelectionCount
|
val defaultPatchSelectionCount by vm.defaultSelectionCount
|
||||||
.collectAsStateWithLifecycle(initialValue = 0)
|
.collectAsStateWithLifecycle(initialValue = 0)
|
||||||
|
|
||||||
@ -105,27 +141,22 @@ fun PatchesSelectorScreen(
|
|||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
FlowRow(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
FilterChip(
|
CheckedFilterChip(
|
||||||
selected = vm.filter and SHOW_SUPPORTED != 0,
|
selected = vm.filter and SHOW_UNSUPPORTED == 0,
|
||||||
onClick = { vm.toggleFlag(SHOW_SUPPORTED) },
|
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
|
||||||
label = { Text(stringResource(R.string.supported)) }
|
label = { Text(stringResource(R.string.supported)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
FilterChip(
|
CheckedFilterChip(
|
||||||
selected = vm.filter and SHOW_UNIVERSAL != 0,
|
selected = vm.filter and SHOW_UNIVERSAL != 0,
|
||||||
onClick = { vm.toggleFlag(SHOW_UNIVERSAL) },
|
onClick = { vm.toggleFlag(SHOW_UNIVERSAL) },
|
||||||
label = { Text(stringResource(R.string.universal)) },
|
label = { Text(stringResource(R.string.universal)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
FilterChip(
|
|
||||||
selected = vm.filter and SHOW_UNSUPPORTED != 0,
|
|
||||||
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
|
|
||||||
label = { Text(stringResource(R.string.unsupported)) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,7 +164,7 @@ fun PatchesSelectorScreen(
|
|||||||
|
|
||||||
if (vm.compatibleVersions.isNotEmpty())
|
if (vm.compatibleVersions.isNotEmpty())
|
||||||
UnsupportedPatchDialog(
|
UnsupportedPatchDialog(
|
||||||
appVersion = vm.appVersion,
|
appVersion = vm.appVersion ?: stringResource(R.string.any_version),
|
||||||
supportedVersions = vm.compatibleVersions,
|
supportedVersions = vm.compatibleVersions,
|
||||||
onDismissRequest = vm::dismissDialogs
|
onDismissRequest = vm::dismissDialogs
|
||||||
)
|
)
|
||||||
@ -142,7 +173,7 @@ fun PatchesSelectorScreen(
|
|||||||
}
|
}
|
||||||
if (showUnsupportedPatchesDialog)
|
if (showUnsupportedPatchesDialog)
|
||||||
UnsupportedPatchesDialog(
|
UnsupportedPatchesDialog(
|
||||||
appVersion = vm.appVersion,
|
appVersion = vm.appVersion ?: stringResource(R.string.any_version),
|
||||||
onDismissRequest = { showUnsupportedPatchesDialog = false }
|
onDismissRequest = { showUnsupportedPatchesDialog = false }
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -172,20 +203,21 @@ fun PatchesSelectorScreen(
|
|||||||
fun LazyListScope.patchList(
|
fun LazyListScope.patchList(
|
||||||
uid: Int,
|
uid: Int,
|
||||||
patches: List<PatchInfo>,
|
patches: List<PatchInfo>,
|
||||||
filterFlag: Int,
|
visible: Boolean,
|
||||||
supported: Boolean,
|
supported: Boolean,
|
||||||
header: (@Composable () -> Unit)? = null
|
header: (@Composable () -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
if (patches.isNotEmpty() && (vm.filter and filterFlag) != 0 || vm.filter == 0) {
|
if (patches.isNotEmpty() && visible) {
|
||||||
header?.let {
|
header?.let {
|
||||||
item {
|
item(contentType = 0) {
|
||||||
it()
|
it()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = patches,
|
items = patches,
|
||||||
key = { it.name }
|
key = { it.name },
|
||||||
|
contentType = { 1 }
|
||||||
) { patch ->
|
) { patch ->
|
||||||
PatchItem(
|
PatchItem(
|
||||||
patch = patch,
|
patch = patch,
|
||||||
@ -219,99 +251,142 @@ fun PatchesSelectorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
search?.let { query ->
|
Scaffold(
|
||||||
SearchView(
|
topBar = {
|
||||||
query = query,
|
SearchBar(
|
||||||
onQueryChange = { search = it },
|
query = query,
|
||||||
onActiveChange = { if (!it) search = null },
|
onQueryChange = setQuery,
|
||||||
placeholder = { Text(stringResource(R.string.search_patches)) }
|
expanded = searchExpanded,
|
||||||
) {
|
onExpandedChange = setSearchExpanded,
|
||||||
val bundle = bundles[pagerState.currentPage]
|
placeholder = {
|
||||||
|
Text(stringResource(R.string.search_patches))
|
||||||
LazyColumnWithScrollbar(
|
},
|
||||||
modifier = Modifier.fillMaxSize()
|
leadingIcon = {
|
||||||
) {
|
val rotation by animateFloatAsState(
|
||||||
fun List<PatchInfo>.searched() = filter {
|
targetValue = if (searchExpanded) 360f else 0f,
|
||||||
it.name.contains(query, true)
|
animationSpec = tween(durationMillis = 400, easing = EaseInOut),
|
||||||
}
|
label = "SearchBar back button"
|
||||||
|
|
||||||
patchList(
|
|
||||||
uid = bundle.uid,
|
|
||||||
patches = bundle.supported.searched(),
|
|
||||||
filterFlag = SHOW_SUPPORTED,
|
|
||||||
supported = true
|
|
||||||
)
|
|
||||||
patchList(
|
|
||||||
uid = bundle.uid,
|
|
||||||
patches = bundle.universal.searched(),
|
|
||||||
filterFlag = SHOW_UNIVERSAL,
|
|
||||||
supported = true
|
|
||||||
) {
|
|
||||||
ListHeader(
|
|
||||||
title = stringResource(R.string.universal_patches),
|
|
||||||
)
|
)
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (searchExpanded) {
|
||||||
|
setSearchExpanded(false)
|
||||||
|
} else {
|
||||||
|
onBackClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.rotate(rotation),
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = searchExpanded,
|
||||||
|
label = "Filter/Clear",
|
||||||
|
transitionSpec = { fadeIn() togetherWith fadeOut() }
|
||||||
|
) { searchExpanded ->
|
||||||
|
if (searchExpanded) {
|
||||||
|
IconButton(
|
||||||
|
onClick = { setQuery("") },
|
||||||
|
enabled = query.isNotEmpty()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Close,
|
||||||
|
contentDescription = stringResource(R.string.clear)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
IconButton(onClick = { showBottomSheet = true }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.FilterList,
|
||||||
|
contentDescription = stringResource(R.string.more)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
val bundle = bundles[pagerState.currentPage]
|
||||||
|
|
||||||
if (!vm.allowIncompatiblePatches) return@LazyColumnWithScrollbar
|
LazyColumnWithScrollbar(
|
||||||
patchList(
|
modifier = Modifier.fillMaxSize()
|
||||||
uid = bundle.uid,
|
|
||||||
patches = bundle.unsupported.searched(),
|
|
||||||
filterFlag = SHOW_UNSUPPORTED,
|
|
||||||
supported = true
|
|
||||||
) {
|
) {
|
||||||
ListHeader(
|
fun List<PatchInfo>.searched() = filter {
|
||||||
title = stringResource(R.string.unsupported_patches),
|
it.name.contains(query, true)
|
||||||
onHelpClick = { showUnsupportedPatchesDialog = true }
|
}
|
||||||
|
|
||||||
|
patchList(
|
||||||
|
uid = bundle.uid,
|
||||||
|
patches = bundle.supported.searched(),
|
||||||
|
visible = true,
|
||||||
|
supported = true
|
||||||
|
)
|
||||||
|
patchList(
|
||||||
|
uid = bundle.uid,
|
||||||
|
patches = bundle.universal.searched(),
|
||||||
|
visible = vm.filter and SHOW_UNIVERSAL != 0,
|
||||||
|
supported = true
|
||||||
|
) {
|
||||||
|
ListHeader(
|
||||||
|
title = stringResource(R.string.universal_patches),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
patchList(
|
||||||
|
uid = bundle.uid,
|
||||||
|
patches = bundle.unsupported.searched(),
|
||||||
|
visible = vm.filter and SHOW_UNSUPPORTED != 0,
|
||||||
|
supported = vm.allowIncompatiblePatches
|
||||||
|
) {
|
||||||
|
ListHeader(
|
||||||
|
title = stringResource(R.string.unsupported_patches),
|
||||||
|
onHelpClick = { showUnsupportedPatchesDialog = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
if (!showSaveButton) return@Scaffold
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = !searchExpanded,
|
||||||
|
enter = slideInHorizontally { it } + fadeIn(),
|
||||||
|
exit = slideOutHorizontally { it } + fadeOut()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
SmallFloatingActionButton(
|
||||||
|
onClick = vm::reset,
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
) {
|
||||||
|
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||||
|
}
|
||||||
|
HapticExtendedFloatingActionButton(
|
||||||
|
text = { Text(stringResource(R.string.save_with_count, selectedPatchCount)) },
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Save,
|
||||||
|
contentDescription = stringResource(R.string.save)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp ?: true,
|
||||||
|
onClick = {
|
||||||
|
onSave(vm.getCustomSelection(), vm.getOptions())
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
AppTopBar(
|
|
||||||
title = stringResource(R.string.patches_selected, selectedPatchCount, availablePatchCount),
|
|
||||||
onBackClick = onBackClick,
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = vm::reset) {
|
|
||||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
|
||||||
}
|
|
||||||
IconButton(onClick = { showBottomSheet = true }) {
|
|
||||||
Icon(Icons.Outlined.FilterList, stringResource(R.string.more))
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
search = ""
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(Icons.Outlined.Search, stringResource(R.string.search))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
floatingActionButton = {
|
|
||||||
if (!showPatchButton) return@Scaffold
|
|
||||||
|
|
||||||
ExtendedFloatingActionButton(
|
|
||||||
text = { Text(stringResource(R.string.save)) },
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
Icons.Outlined.Save,
|
|
||||||
stringResource(R.string.save)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
|
|
||||||
?: true,
|
|
||||||
onClick = {
|
|
||||||
// TODO: only allow this if all required options have been set.
|
|
||||||
onSave(vm.getCustomSelection(), vm.getOptions())
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
@ -321,7 +396,7 @@ fun PatchesSelectorScreen(
|
|||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
) {
|
) {
|
||||||
bundles.forEachIndexed { index, bundle ->
|
bundles.forEachIndexed { index, bundle ->
|
||||||
Tab(
|
HapticTab(
|
||||||
selected = pagerState.currentPage == index,
|
selected = pagerState.currentPage == index,
|
||||||
onClick = {
|
onClick = {
|
||||||
composableScope.launch {
|
composableScope.launch {
|
||||||
@ -353,13 +428,13 @@ fun PatchesSelectorScreen(
|
|||||||
patchList(
|
patchList(
|
||||||
uid = bundle.uid,
|
uid = bundle.uid,
|
||||||
patches = bundle.supported,
|
patches = bundle.supported,
|
||||||
filterFlag = SHOW_SUPPORTED,
|
visible = true,
|
||||||
supported = true
|
supported = true
|
||||||
)
|
)
|
||||||
patchList(
|
patchList(
|
||||||
uid = bundle.uid,
|
uid = bundle.uid,
|
||||||
patches = bundle.universal,
|
patches = bundle.universal,
|
||||||
filterFlag = SHOW_UNIVERSAL,
|
visible = vm.filter and SHOW_UNIVERSAL != 0,
|
||||||
supported = true
|
supported = true
|
||||||
) {
|
) {
|
||||||
ListHeader(
|
ListHeader(
|
||||||
@ -369,7 +444,7 @@ fun PatchesSelectorScreen(
|
|||||||
patchList(
|
patchList(
|
||||||
uid = bundle.uid,
|
uid = bundle.uid,
|
||||||
patches = bundle.unsupported,
|
patches = bundle.unsupported,
|
||||||
filterFlag = SHOW_UNSUPPORTED,
|
visible = vm.filter and SHOW_UNSUPPORTED != 0,
|
||||||
supported = vm.allowIncompatiblePatches
|
supported = vm.allowIncompatiblePatches
|
||||||
) {
|
) {
|
||||||
ListHeader(
|
ListHeader(
|
||||||
@ -438,7 +513,7 @@ private fun PatchItem(
|
|||||||
.clickable(onClick = onToggle)
|
.clickable(onClick = onToggle)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Checkbox(
|
HapticCheckbox(
|
||||||
checked = selected,
|
checked = selected,
|
||||||
onCheckedChange = { onToggle() },
|
onCheckedChange = { onToggle() },
|
||||||
enabled = supported
|
enabled = supported
|
||||||
@ -452,11 +527,12 @@ private fun PatchItem(
|
|||||||
Icon(Icons.Outlined.Settings, null)
|
Icon(Icons.Outlined.Settings, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
colors = transparentListItemColors
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ListHeader(
|
fun ListHeader(
|
||||||
title: String,
|
title: String,
|
||||||
onHelpClick: (() -> Unit)? = null
|
onHelpClick: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
@ -477,7 +553,8 @@ private fun ListHeader(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
colors = transparentListItemColors
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,158 @@
|
|||||||
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.ScrollableTabRow
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.patcher.patch.Option
|
||||||
|
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.component.haptics.HapticTab
|
||||||
|
import app.revanced.manager.ui.component.patches.OptionItem
|
||||||
|
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
|
||||||
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||||
|
import app.revanced.manager.util.Options
|
||||||
|
import app.revanced.manager.util.PatchSelection
|
||||||
|
import app.revanced.manager.util.isScrollingUp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RequiredOptionsScreen(
|
||||||
|
onContinue: (PatchSelection?, Options) -> Unit,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
vm: PatchesSelectorViewModel
|
||||||
|
) {
|
||||||
|
val list by vm.requiredOptsPatches.collectAsStateWithLifecycle(emptyList())
|
||||||
|
|
||||||
|
val pagerState = rememberPagerState(
|
||||||
|
initialPage = 0,
|
||||||
|
initialPageOffsetFraction = 0f
|
||||||
|
) {
|
||||||
|
list.size
|
||||||
|
}
|
||||||
|
val patchLazyListStates = remember(list) { List(list.size, ::LazyListState) }
|
||||||
|
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(emptyList())
|
||||||
|
val showContinueButton by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
bundles.requiredOptionsSet(
|
||||||
|
isSelected = { bundle, patch -> vm.isSelected(bundle.uid, patch) },
|
||||||
|
optionsForPatch = { bundle, patch -> vm.getOptions(bundle.uid, patch) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val composableScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
AppTopBar(
|
||||||
|
title = stringResource(R.string.required_options_screen),
|
||||||
|
onBackClick = onBackClick
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
if (!showContinueButton) return@Scaffold
|
||||||
|
|
||||||
|
HapticExtendedFloatingActionButton(
|
||||||
|
text = { Text(stringResource(R.string.patch)) },
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.AutoFixHigh,
|
||||||
|
stringResource(R.string.patch)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
|
||||||
|
?: true,
|
||||||
|
onClick = {
|
||||||
|
onContinue(vm.getCustomSelection(), vm.getOptions())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
if (list.isEmpty()) return@Column
|
||||||
|
else if (list.size > 1) ScrollableTabRow(
|
||||||
|
selectedTabIndex = pagerState.currentPage,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||||
|
) {
|
||||||
|
list.forEachIndexed { index, (bundle, _) ->
|
||||||
|
HapticTab(
|
||||||
|
selected = pagerState.currentPage == index,
|
||||||
|
onClick = {
|
||||||
|
composableScope.launch {
|
||||||
|
pagerState.animateScrollToPage(
|
||||||
|
index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = { Text(bundle.name) },
|
||||||
|
selectedContentColor = MaterialTheme.colorScheme.primary,
|
||||||
|
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
userScrollEnabled = true,
|
||||||
|
pageContent = { index ->
|
||||||
|
// Avoid crashing if the lists have not been fully initialized yet.
|
||||||
|
if (index > list.lastIndex || list.size != patchLazyListStates.size) return@HorizontalPager
|
||||||
|
val (bundle, patches) = list[index]
|
||||||
|
|
||||||
|
LazyColumnWithScrollbar(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = patchLazyListStates[index]
|
||||||
|
) {
|
||||||
|
items(patches, key = { it.name }) {
|
||||||
|
ListHeader(it.name)
|
||||||
|
|
||||||
|
val values = vm.getOptions(bundle.uid, it)
|
||||||
|
it.options?.forEach { option ->
|
||||||
|
val key = option.key
|
||||||
|
val value =
|
||||||
|
if (values == null || key !in values) option.default else values[key]
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
OptionItem(
|
||||||
|
option = option as Option<Any>,
|
||||||
|
value = value,
|
||||||
|
setValue = { new ->
|
||||||
|
vm.setOption(bundle.uid, it, key, new)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,52 +1,53 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
import android.content.pm.PackageInfo
|
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.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
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.ui.Modifier
|
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.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.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.destination.SelectedAppInfoDestination
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
|
||||||
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.toast
|
import app.revanced.manager.util.toast
|
||||||
import dev.olshevski.navigation.reimagined.AnimatedNavHost
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
import dev.olshevski.navigation.reimagined.NavBackHandler
|
import kotlinx.coroutines.launch
|
||||||
import dev.olshevski.navigation.reimagined.navigate
|
|
||||||
import dev.olshevski.navigation.reimagined.pop
|
|
||||||
import dev.olshevski.navigation.reimagined.rememberNavController
|
|
||||||
import org.koin.androidx.compose.koinViewModel
|
|
||||||
import org.koin.core.parameter.parametersOf
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectedAppInfoScreen(
|
fun SelectedAppInfoScreen(
|
||||||
onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit,
|
onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit,
|
||||||
|
onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit,
|
||||||
|
onPatchClick: () -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: SelectedAppInfoViewModel
|
vm: SelectedAppInfoViewModel
|
||||||
) {
|
) {
|
||||||
@ -54,105 +55,26 @@ fun SelectedAppInfoScreen(
|
|||||||
|
|
||||||
val packageName = vm.selectedApp.packageName
|
val packageName = vm.selectedApp.packageName
|
||||||
val version = vm.selectedApp.version
|
val version = vm.selectedApp.version
|
||||||
val bundles by remember(packageName, version) {
|
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
|
||||||
vm.bundlesRepo.bundleInfoFlow(packageName, version)
|
|
||||||
}.collectAsStateWithLifecycle(initialValue = emptyList())
|
|
||||||
|
|
||||||
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
||||||
val patches by remember {
|
val patches = remember(bundles, allowIncompatiblePatches) {
|
||||||
derivedStateOf {
|
vm.getPatches(bundles, allowIncompatiblePatches)
|
||||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val selectedPatchCount by remember {
|
val selectedPatchCount = remember(patches) {
|
||||||
derivedStateOf {
|
patches.values.sumOf { it.size }
|
||||||
patches.values.sumOf { it.size }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val navController =
|
val launcher = rememberLauncherForActivityResult(
|
||||||
rememberNavController<SelectedAppInfoDestination>(startDestination = SelectedAppInfoDestination.Main)
|
contract = ActivityResultContracts.StartActivityForResult(),
|
||||||
|
onResult = vm::handlePluginActivityResult
|
||||||
NavBackHandler(controller = navController)
|
)
|
||||||
|
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
||||||
AnimatedNavHost(controller = navController) { destination ->
|
launcher.launch(intent)
|
||||||
when (destination) {
|
|
||||||
is SelectedAppInfoDestination.Main -> SelectedAppInfoScreen(
|
|
||||||
onPatchClick = patchClick@{
|
|
||||||
if (selectedPatchCount == 0) {
|
|
||||||
context.toast(context.getString(R.string.no_patches_selected))
|
|
||||||
|
|
||||||
return@patchClick
|
|
||||||
}
|
|
||||||
onPatchClick(
|
|
||||||
vm.selectedApp,
|
|
||||||
patches,
|
|
||||||
vm.getOptionsFiltered(bundles)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onPatchSelectorClick = {
|
|
||||||
navController.navigate(
|
|
||||||
SelectedAppInfoDestination.PatchesSelector(
|
|
||||||
vm.selectedApp,
|
|
||||||
vm.getCustomPatches(
|
|
||||||
bundles,
|
|
||||||
allowIncompatiblePatches
|
|
||||||
),
|
|
||||||
vm.options
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onVersionSelectorClick = {
|
|
||||||
navController.navigate(SelectedAppInfoDestination.VersionSelector)
|
|
||||||
},
|
|
||||||
onBackClick = onBackClick,
|
|
||||||
selectedPatchCount = selectedPatchCount,
|
|
||||||
packageName = packageName,
|
|
||||||
version = version,
|
|
||||||
packageInfo = vm.selectedAppInfo,
|
|
||||||
)
|
|
||||||
|
|
||||||
is SelectedAppInfoDestination.VersionSelector -> VersionSelectorScreen(
|
|
||||||
onBackClick = navController::pop,
|
|
||||||
onAppClick = {
|
|
||||||
vm.selectedApp = it
|
|
||||||
navController.pop()
|
|
||||||
},
|
|
||||||
viewModel = koinViewModel { parametersOf(packageName) }
|
|
||||||
)
|
|
||||||
|
|
||||||
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
|
|
||||||
onSave = { patches, options ->
|
|
||||||
vm.updateConfiguration(patches, options, bundles)
|
|
||||||
navController.pop()
|
|
||||||
},
|
|
||||||
onBackClick = navController::pop,
|
|
||||||
vm = koinViewModel {
|
|
||||||
parametersOf(
|
|
||||||
PatchesSelectorViewModel.Params(
|
|
||||||
destination.app,
|
|
||||||
destination.currentSelection,
|
|
||||||
destination.options,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
val composableScope = rememberCoroutineScope()
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
|
||||||
@Composable
|
|
||||||
private fun SelectedAppInfoScreen(
|
|
||||||
onPatchClick: () -> Unit,
|
|
||||||
onPatchSelectorClick: () -> Unit,
|
|
||||||
onVersionSelectorClick: () -> Unit,
|
|
||||||
onBackClick: () -> Unit,
|
|
||||||
selectedPatchCount: Int,
|
|
||||||
packageName: String,
|
|
||||||
version: String,
|
|
||||||
packageInfo: PackageInfo?,
|
|
||||||
) {
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
@ -161,7 +83,9 @@ private fun SelectedAppInfoScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
ExtendedFloatingActionButton(
|
if (error != null) return@Scaffold
|
||||||
|
|
||||||
|
HapticExtendedFloatingActionButton(
|
||||||
text = { Text(stringResource(R.string.patch)) },
|
text = { Text(stringResource(R.string.patch)) },
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
@ -169,18 +93,61 @@ private fun SelectedAppInfoScreen(
|
|||||||
stringResource(R.string.patch)
|
stringResource(R.string.patch)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = onPatchClick
|
onClick = patchClick@{
|
||||||
|
if (selectedPatchCount == 0) {
|
||||||
|
context.toast(context.getString(R.string.no_patches_selected))
|
||||||
|
|
||||||
|
return@patchClick
|
||||||
|
}
|
||||||
|
|
||||||
|
composableScope.launch {
|
||||||
|
if (!vm.hasSetRequiredOptions(patches)) {
|
||||||
|
onRequiredOptions(
|
||||||
|
vm.selectedApp,
|
||||||
|
vm.getCustomPatches(bundles, allowIncompatiblePatches),
|
||||||
|
vm.options
|
||||||
|
)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
onPatchClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { 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(packageInfo, placeholderLabel = packageName) {
|
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.selected_app_meta, version),
|
version ?: stringResource(R.string.selected_app_meta_any_version),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
@ -188,14 +155,45 @@ private fun SelectedAppInfoScreen(
|
|||||||
|
|
||||||
PageItem(
|
PageItem(
|
||||||
R.string.patch_selector_item,
|
R.string.patch_selector_item,
|
||||||
stringResource(R.string.patch_selector_item_description, selectedPatchCount),
|
stringResource(
|
||||||
onPatchSelectorClick
|
R.string.patch_selector_item_description,
|
||||||
|
selectedPatchCount
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
onPatchSelectorClick(
|
||||||
|
vm.selectedApp,
|
||||||
|
vm.getCustomPatches(
|
||||||
|
bundles,
|
||||||
|
allowIncompatiblePatches
|
||||||
|
),
|
||||||
|
vm.options
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
PageItem(
|
PageItem(
|
||||||
R.string.version_selector_item,
|
R.string.apk_source_selector_item,
|
||||||
stringResource(R.string.version_selector_item_description, version),
|
when (val app = vm.selectedApp) {
|
||||||
onVersionSelectorClick
|
is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
|
||||||
|
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
|
||||||
|
is SelectedApp.Download -> stringResource(
|
||||||
|
R.string.apk_source_downloader,
|
||||||
|
plugins.find { it.packageName == app.data.pluginPackageName }?.name
|
||||||
|
?: app.data.pluginPackageName
|
||||||
|
)
|
||||||
|
|
||||||
|
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
vm.showSourceSelector()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
error?.let {
|
||||||
|
Text(
|
||||||
|
stringResource(it.resourceId),
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,3 +223,88 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () ->
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
@ -13,146 +13,64 @@ import app.revanced.manager.R
|
|||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.destination.SettingsDestination
|
import app.revanced.manager.ui.model.navigation.Settings
|
||||||
import app.revanced.manager.ui.screen.settings.*
|
|
||||||
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
|
private val settingsSections = listOf(
|
||||||
import app.revanced.manager.ui.screen.settings.update.UpdateScreen
|
Triple(
|
||||||
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
|
R.string.general,
|
||||||
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
R.string.general_description,
|
||||||
import dev.olshevski.navigation.reimagined.*
|
Icons.Outlined.Settings
|
||||||
import org.koin.androidx.compose.koinViewModel
|
) to Settings.General,
|
||||||
import org.koin.core.parameter.parametersOf
|
Triple(
|
||||||
|
R.string.updates,
|
||||||
|
R.string.updates_description,
|
||||||
|
Icons.Outlined.Update
|
||||||
|
) to Settings.Updates,
|
||||||
|
Triple(
|
||||||
|
R.string.downloads,
|
||||||
|
R.string.downloads_description,
|
||||||
|
Icons.Outlined.Download
|
||||||
|
) to Settings.Downloads,
|
||||||
|
Triple(
|
||||||
|
R.string.import_export,
|
||||||
|
R.string.import_export_description,
|
||||||
|
Icons.Outlined.SwapVert
|
||||||
|
) to Settings.ImportExport,
|
||||||
|
Triple(
|
||||||
|
R.string.advanced,
|
||||||
|
R.string.advanced_description,
|
||||||
|
Icons.Outlined.Tune
|
||||||
|
) to Settings.Advanced,
|
||||||
|
Triple(
|
||||||
|
R.string.about,
|
||||||
|
R.string.app_name,
|
||||||
|
Icons.Outlined.Info
|
||||||
|
) to Settings.About,
|
||||||
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> Unit) {
|
||||||
onBackClick: () -> Unit,
|
Scaffold(
|
||||||
startDestination: SettingsDestination,
|
topBar = {
|
||||||
viewModel: SettingsViewModel = koinViewModel()
|
AppTopBar(
|
||||||
) {
|
title = stringResource(R.string.settings),
|
||||||
val navController = rememberNavController(startDestination)
|
onBackClick = onBackClick,
|
||||||
|
|
||||||
val backClick: () -> Unit = {
|
|
||||||
if (navController.backstack.entries.size == 1)
|
|
||||||
onBackClick()
|
|
||||||
else navController.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
val settingsSections = listOf(
|
|
||||||
Triple(
|
|
||||||
R.string.general,
|
|
||||||
R.string.general_description,
|
|
||||||
Icons.Outlined.Settings
|
|
||||||
) to SettingsDestination.General,
|
|
||||||
Triple(
|
|
||||||
R.string.updates,
|
|
||||||
R.string.updates_description,
|
|
||||||
Icons.Outlined.Update
|
|
||||||
) to SettingsDestination.Updates,
|
|
||||||
Triple(
|
|
||||||
R.string.downloads,
|
|
||||||
R.string.downloads_description,
|
|
||||||
Icons.Outlined.Download
|
|
||||||
) to SettingsDestination.Downloads,
|
|
||||||
Triple(
|
|
||||||
R.string.import_export,
|
|
||||||
R.string.import_export_description,
|
|
||||||
Icons.Outlined.SwapVert
|
|
||||||
) to SettingsDestination.ImportExport,
|
|
||||||
Triple(
|
|
||||||
R.string.advanced,
|
|
||||||
R.string.advanced_description,
|
|
||||||
Icons.Outlined.Tune
|
|
||||||
) to SettingsDestination.Advanced,
|
|
||||||
Triple(
|
|
||||||
R.string.about,
|
|
||||||
R.string.app_name,
|
|
||||||
Icons.Outlined.Info
|
|
||||||
) to SettingsDestination.About,
|
|
||||||
)
|
|
||||||
NavBackHandler(navController)
|
|
||||||
|
|
||||||
AnimatedNavHost(
|
|
||||||
controller = navController
|
|
||||||
) { destination ->
|
|
||||||
when (destination) {
|
|
||||||
is SettingsDestination.General -> GeneralSettingsScreen(
|
|
||||||
onBackClick = backClick,
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
is SettingsDestination.Advanced -> AdvancedSettingsScreen(
|
) { paddingValues ->
|
||||||
onBackClick = backClick
|
ColumnWithScrollbar(
|
||||||
)
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
is SettingsDestination.Updates -> UpdatesSettingsScreen(
|
.fillMaxSize()
|
||||||
onBackClick = backClick,
|
) {
|
||||||
onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) },
|
settingsSections.forEach { (titleDescIcon, destination) ->
|
||||||
onUpdateClick = { navController.navigate(SettingsDestination.Update(false)) }
|
SettingsListItem(
|
||||||
)
|
modifier = Modifier.clickable { navigate(destination) },
|
||||||
|
headlineContent = stringResource(titleDescIcon.first),
|
||||||
is SettingsDestination.Downloads -> DownloadsSettingsScreen(
|
supportingContent = stringResource(titleDescIcon.second),
|
||||||
onBackClick = backClick
|
leadingContent = { Icon(titleDescIcon.third, null) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsDestination.ImportExport -> ImportExportSettingsScreen(
|
|
||||||
onBackClick = backClick
|
|
||||||
)
|
|
||||||
|
|
||||||
is SettingsDestination.About -> AboutSettingsScreen(
|
|
||||||
onBackClick = backClick,
|
|
||||||
onContributorsClick = { navController.navigate(SettingsDestination.Contributors) },
|
|
||||||
onDeveloperOptionsClick = { navController.navigate(SettingsDestination.DeveloperOptions) },
|
|
||||||
onLicensesClick = { navController.navigate(SettingsDestination.Licenses) },
|
|
||||||
)
|
|
||||||
|
|
||||||
is SettingsDestination.Update -> UpdateScreen(
|
|
||||||
onBackClick = backClick,
|
|
||||||
vm = koinViewModel {
|
|
||||||
parametersOf(
|
|
||||||
destination.downloadOnScreenEntry
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
is SettingsDestination.Changelogs -> ChangelogsScreen(
|
|
||||||
onBackClick = backClick,
|
|
||||||
)
|
|
||||||
|
|
||||||
is SettingsDestination.Contributors -> ContributorScreen(
|
|
||||||
onBackClick = backClick,
|
|
||||||
)
|
|
||||||
|
|
||||||
is SettingsDestination.Licenses -> LicensesScreen(
|
|
||||||
onBackClick = backClick,
|
|
||||||
)
|
|
||||||
|
|
||||||
is SettingsDestination.DeveloperOptions -> DeveloperOptionsScreen(onBackClick = backClick)
|
|
||||||
|
|
||||||
is SettingsDestination.Settings -> {
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
AppTopBar(
|
|
||||||
title = stringResource(R.string.settings),
|
|
||||||
onBackClick = backClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
ColumnWithScrollbar(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
settingsSections.forEach { (titleDescIcon, destination) ->
|
|
||||||
SettingsListItem(
|
|
||||||
modifier = Modifier.clickable { navController.navigate(destination) },
|
|
||||||
headlineContent = stringResource(titleDescIcon.first),
|
|
||||||
supportingContent = stringResource(titleDescIcon.second),
|
|
||||||
leadingContent = { Icon(titleDescIcon.third, null) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package app.revanced.manager.ui.screen.settings.update
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
@ -33,12 +33,11 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.BuildConfig
|
import app.revanced.manager.BuildConfig
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.settings.Changelog
|
import app.revanced.manager.ui.component.settings.Changelog
|
||||||
import app.revanced.manager.ui.viewmodel.UpdateViewModel
|
import app.revanced.manager.ui.viewmodel.UpdateViewModel
|
||||||
import app.revanced.manager.ui.viewmodel.UpdateViewModel.Changelog
|
|
||||||
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
|
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
|
||||||
import app.revanced.manager.util.formatNumber
|
|
||||||
import app.revanced.manager.util.relativeTime
|
import app.revanced.manager.util.relativeTime
|
||||||
import com.gigamole.composefadingedges.content.FadingEdgesContentType
|
import com.gigamole.composefadingedges.content.FadingEdgesContentType
|
||||||
import com.gigamole.composefadingedges.content.scrollconfig.FadingEdgesScrollConfig
|
import com.gigamole.composefadingedges.content.scrollconfig.FadingEdgesScrollConfig
|
||||||
@ -77,10 +76,10 @@ fun UpdateScreen(
|
|||||||
) {
|
) {
|
||||||
Header(
|
Header(
|
||||||
vm.state,
|
vm.state,
|
||||||
vm.changelog,
|
vm.releaseInfo,
|
||||||
DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize)
|
DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize)
|
||||||
)
|
)
|
||||||
vm.changelog?.let { changelog ->
|
vm.releaseInfo?.let { changelog ->
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
Changelog(changelog)
|
Changelog(changelog)
|
||||||
} ?: Spacer(modifier = Modifier.weight(1f))
|
} ?: Spacer(modifier = Modifier.weight(1f))
|
||||||
@ -118,7 +117,7 @@ private fun MeteredDownloadConfirmationDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Header(state: State, changelog: Changelog?, downloadData: DownloadData) {
|
private fun Header(state: State, releaseInfo: ReVancedAsset?, downloadData: DownloadData) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(state.title),
|
text = stringResource(state.title),
|
||||||
@ -134,11 +133,11 @@ private fun Header(state: State, changelog: Changelog?, downloadData: DownloadDa
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
changelog?.let { changelog ->
|
releaseInfo?.version?.let {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(
|
text = stringResource(
|
||||||
id = R.string.new_version,
|
R.string.new_version,
|
||||||
changelog.version.replace("v", "")
|
it.replace("v", "")
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
@ -170,7 +169,7 @@ private fun Header(state: State, changelog: Changelog?, downloadData: DownloadDa
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.Changelog(changelog: Changelog) {
|
private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -194,10 +193,9 @@ private fun ColumnScope.Changelog(changelog: Changelog) {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Changelog(
|
Changelog(
|
||||||
markdown = changelog.body.replace("`", ""),
|
markdown = releaseInfo.description.replace("`", ""),
|
||||||
version = changelog.version,
|
version = releaseInfo.version,
|
||||||
downloadCount = changelog.downloadCount.formatNumber(),
|
publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current)
|
||||||
publishDate = changelog.publishDate.relativeTime(LocalContext.current)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,201 +0,0 @@
|
|||||||
package app.revanced.manager.ui.screen
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
|
||||||
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.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
|
||||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
|
||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
|
||||||
import app.revanced.manager.ui.component.NonSuggestedVersionDialog
|
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
|
||||||
import app.revanced.manager.ui.viewmodel.VersionSelectorViewModel
|
|
||||||
import app.revanced.manager.util.isScrollingUp
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun VersionSelectorScreen(
|
|
||||||
onBackClick: () -> Unit,
|
|
||||||
onAppClick: (SelectedApp) -> Unit,
|
|
||||||
viewModel: VersionSelectorViewModel
|
|
||||||
) {
|
|
||||||
val supportedVersions by viewModel.supportedVersions.collectAsStateWithLifecycle(emptyMap())
|
|
||||||
val downloadedVersions by viewModel.downloadedVersions.collectAsStateWithLifecycle(emptyList())
|
|
||||||
|
|
||||||
val list by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
val apps = (downloadedVersions + viewModel.downloadableVersions)
|
|
||||||
.distinctBy { it.version }
|
|
||||||
.sortedWith(
|
|
||||||
compareByDescending<SelectedApp> {
|
|
||||||
it is SelectedApp.Local
|
|
||||||
}.thenByDescending { supportedVersions[it.version] }
|
|
||||||
.thenByDescending { it.version }
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.requiredVersion?.let { requiredVersion ->
|
|
||||||
apps.filter { it.version == requiredVersion }
|
|
||||||
} ?: apps
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewModel.showNonSuggestedVersionDialog)
|
|
||||||
NonSuggestedVersionDialog(
|
|
||||||
suggestedVersion = viewModel.requiredVersion.orEmpty(),
|
|
||||||
onDismiss = viewModel::dismissNonSuggestedVersionDialog
|
|
||||||
)
|
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
AppTopBar(
|
|
||||||
title = stringResource(R.string.select_version),
|
|
||||||
onBackClick = onBackClick,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
floatingActionButton = {
|
|
||||||
ExtendedFloatingActionButton(
|
|
||||||
text = { Text(stringResource(R.string.select_version)) },
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Check,
|
|
||||||
stringResource(R.string.select_version)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
expanded = lazyListState.isScrollingUp,
|
|
||||||
onClick = { viewModel.selectedVersion?.let(onAppClick) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
LazyColumnWithScrollbar(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
.fillMaxSize(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
state = lazyListState
|
|
||||||
) {
|
|
||||||
viewModel.installedApp?.let { (packageInfo, installedApp) ->
|
|
||||||
SelectedApp.Installed(
|
|
||||||
packageName = viewModel.packageName,
|
|
||||||
version = packageInfo.versionName
|
|
||||||
).let {
|
|
||||||
item {
|
|
||||||
SelectedAppItem(
|
|
||||||
selectedApp = it,
|
|
||||||
selected = viewModel.selectedVersion == it,
|
|
||||||
onClick = { viewModel.select(it) },
|
|
||||||
patchCount = supportedVersions[it.version],
|
|
||||||
enabled =
|
|
||||||
!(installedApp?.installType == InstallType.MOUNT && !viewModel.rootInstaller.hasRootAccess()),
|
|
||||||
alreadyPatched = installedApp != null && installedApp.installType != InstallType.MOUNT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
Row(Modifier.fillMaxWidth()) {
|
|
||||||
GroupHeader(stringResource(R.string.downloadable_versions))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items(
|
|
||||||
items = list,
|
|
||||||
key = { it.version }
|
|
||||||
) {
|
|
||||||
SelectedAppItem(
|
|
||||||
selectedApp = it,
|
|
||||||
selected = viewModel.selectedVersion == it,
|
|
||||||
onClick = { viewModel.select(it) },
|
|
||||||
patchCount = supportedVersions[it.version]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewModel.errorMessage != null) {
|
|
||||||
item {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.error_occurred))
|
|
||||||
Text(
|
|
||||||
text = viewModel.errorMessage!!,
|
|
||||||
modifier = Modifier.padding(horizontal = 15.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (viewModel.isLoading) {
|
|
||||||
item {
|
|
||||||
LoadingIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SelectedAppItem(
|
|
||||||
selectedApp: SelectedApp,
|
|
||||||
selected: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
patchCount: Int?,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
alreadyPatched: Boolean = false,
|
|
||||||
) {
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { RadioButton(selected, null) },
|
|
||||||
headlineContent = { Text(selectedApp.version) },
|
|
||||||
supportingContent = when (selectedApp) {
|
|
||||||
is SelectedApp.Installed ->
|
|
||||||
if (alreadyPatched) {
|
|
||||||
{ Text(stringResource(R.string.already_patched)) }
|
|
||||||
} else {
|
|
||||||
{ Text(stringResource(R.string.installed)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
is SelectedApp.Local -> {
|
|
||||||
{ Text(stringResource(R.string.already_downloaded)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
trailingContent = patchCount?.let {
|
|
||||||
{
|
|
||||||
Text(pluralStringResource(R.plurals.patch_count, it, it))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(enabled = !alreadyPatched && enabled, onClick = onClick)
|
|
||||||
.run {
|
|
||||||
if (!enabled || alreadyPatched) alpha(0.5f)
|
|
||||||
else this
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
@ -37,6 +37,7 @@ import app.revanced.manager.network.dto.ReVancedSocial
|
|||||||
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.settings.SettingsListItem
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
|
import app.revanced.manager.ui.model.navigation.Settings
|
||||||
import app.revanced.manager.ui.viewmodel.AboutViewModel
|
import app.revanced.manager.ui.viewmodel.AboutViewModel
|
||||||
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
|
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
|
||||||
import app.revanced.manager.util.openUrl
|
import app.revanced.manager.util.openUrl
|
||||||
@ -47,9 +48,7 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun AboutSettingsScreen(
|
fun AboutSettingsScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onContributorsClick: () -> Unit,
|
navigate: (Settings.Destination) -> Unit,
|
||||||
onLicensesClick: () -> Unit,
|
|
||||||
onDeveloperOptionsClick: () -> Unit,
|
|
||||||
viewModel: AboutViewModel = koinViewModel()
|
viewModel: AboutViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@ -114,17 +113,17 @@ fun AboutSettingsScreen(
|
|||||||
Triple(
|
Triple(
|
||||||
stringResource(R.string.contributors),
|
stringResource(R.string.contributors),
|
||||||
stringResource(R.string.contributors_description),
|
stringResource(R.string.contributors_description),
|
||||||
third = onContributorsClick
|
third = { navigate(Settings.Contributors) }
|
||||||
),
|
),
|
||||||
Triple(
|
Triple(
|
||||||
stringResource(R.string.developer_options),
|
stringResource(R.string.developer_options),
|
||||||
stringResource(R.string.developer_options_description),
|
stringResource(R.string.developer_options_description),
|
||||||
third = onDeveloperOptionsClick
|
third = { navigate(Settings.DeveloperOptions) }
|
||||||
),
|
),
|
||||||
Triple(
|
Triple(
|
||||||
stringResource(R.string.opensource_licenses),
|
stringResource(R.string.opensource_licenses),
|
||||||
stringResource(R.string.opensource_licenses_description),
|
stringResource(R.string.opensource_licenses_description),
|
||||||
third = onLicensesClick
|
third = { navigate(Settings.Licenses) }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import android.app.ActivityManager
|
|||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
@ -17,9 +18,7 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
|
||||||
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
|
||||||
@ -32,9 +31,11 @@ import app.revanced.manager.ui.component.ColumnWithScrollbar
|
|||||||
import app.revanced.manager.ui.component.GroupHeader
|
import app.revanced.manager.ui.component.GroupHeader
|
||||||
import app.revanced.manager.ui.component.settings.BooleanItem
|
import app.revanced.manager.ui.component.settings.BooleanItem
|
||||||
import app.revanced.manager.ui.component.settings.IntegerItem
|
import app.revanced.manager.ui.component.settings.IntegerItem
|
||||||
|
import app.revanced.manager.ui.component.settings.SafeguardBooleanItem
|
||||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
|
import app.revanced.manager.util.withHapticFeedback
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@ -52,7 +53,6 @@ fun AdvancedSettingsScreen(
|
|||||||
activityManager.largeMemoryClass
|
activityManager.largeMemoryClass
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val haptics = LocalHapticFeedback.current
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -103,37 +103,35 @@ fun AdvancedSettingsScreen(
|
|||||||
headline = R.string.process_runtime_memory_limit,
|
headline = R.string.process_runtime_memory_limit,
|
||||||
description = R.string.process_runtime_memory_limit_description,
|
description = R.string.process_runtime_memory_limit_description,
|
||||||
)
|
)
|
||||||
BooleanItem(
|
|
||||||
preference = vm.prefs.multithreadingDexFileWriter,
|
|
||||||
coroutineScope = vm.viewModelScope,
|
|
||||||
headline = R.string.multithreaded_dex_file_writer,
|
|
||||||
description = R.string.multithreaded_dex_file_writer_description,
|
|
||||||
)
|
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.safeguards))
|
GroupHeader(stringResource(R.string.safeguards))
|
||||||
BooleanItem(
|
SafeguardBooleanItem(
|
||||||
preference = vm.prefs.disablePatchVersionCompatCheck,
|
preference = vm.prefs.disablePatchVersionCompatCheck,
|
||||||
coroutineScope = vm.viewModelScope,
|
coroutineScope = vm.viewModelScope,
|
||||||
headline = R.string.patch_compat_check,
|
headline = R.string.patch_compat_check,
|
||||||
description = R.string.patch_compat_check_description
|
description = R.string.patch_compat_check_description,
|
||||||
|
confirmationText = R.string.patch_compat_check_confirmation
|
||||||
)
|
)
|
||||||
BooleanItem(
|
SafeguardBooleanItem(
|
||||||
preference = vm.prefs.disableUniversalPatchWarning,
|
preference = vm.prefs.disableUniversalPatchWarning,
|
||||||
coroutineScope = vm.viewModelScope,
|
coroutineScope = vm.viewModelScope,
|
||||||
headline = R.string.universal_patches_safeguard,
|
headline = R.string.universal_patches_safeguard,
|
||||||
description = R.string.universal_patches_safeguard_description
|
description = R.string.universal_patches_safeguard_description,
|
||||||
|
confirmationText = R.string.universal_patches_safeguard_confirmation
|
||||||
)
|
)
|
||||||
BooleanItem(
|
SafeguardBooleanItem(
|
||||||
preference = vm.prefs.suggestedVersionSafeguard,
|
preference = vm.prefs.suggestedVersionSafeguard,
|
||||||
coroutineScope = vm.viewModelScope,
|
coroutineScope = vm.viewModelScope,
|
||||||
headline = R.string.suggested_version_safeguard,
|
headline = R.string.suggested_version_safeguard,
|
||||||
description = R.string.suggested_version_safeguard_description
|
description = R.string.suggested_version_safeguard_description,
|
||||||
|
confirmationText = R.string.suggested_version_safeguard_confirmation
|
||||||
)
|
)
|
||||||
BooleanItem(
|
SafeguardBooleanItem(
|
||||||
preference = vm.prefs.disableSelectionWarning,
|
preference = vm.prefs.disableSelectionWarning,
|
||||||
coroutineScope = vm.viewModelScope,
|
coroutineScope = vm.viewModelScope,
|
||||||
headline = R.string.patch_selection_safeguard,
|
headline = R.string.patch_selection_safeguard,
|
||||||
description = R.string.patch_selection_safeguard_description
|
description = R.string.patch_selection_safeguard_description,
|
||||||
|
confirmationText = R.string.patch_selection_safeguard_confirmation
|
||||||
)
|
)
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.debugging))
|
GroupHeader(stringResource(R.string.debugging))
|
||||||
@ -159,13 +157,12 @@ fun AdvancedSettingsScreen(
|
|||||||
onClick = { },
|
onClick = { },
|
||||||
onLongClickLabel = stringResource(R.string.copy_to_clipboard),
|
onLongClickLabel = stringResource(R.string.copy_to_clipboard),
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
clipboard.setPrimaryClip(
|
clipboard.setPrimaryClip(
|
||||||
ClipData.newPlainText("Device Information", deviceContent)
|
ClipData.newPlainText("Device Information", deviceContent)
|
||||||
)
|
)
|
||||||
|
|
||||||
context.toast(context.getString(R.string.toast_copied_to_clipboard))
|
context.toast(context.getString(R.string.toast_copied_to_clipboard))
|
||||||
}
|
}.withHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
),
|
),
|
||||||
headlineContent = stringResource(R.string.about_device),
|
headlineContent = stringResource(R.string.about_device),
|
||||||
supportingContent = deviceContent
|
supportingContent = deviceContent
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user