Compare commits

...

14 Commits

Author SHA1 Message Date
Ax333l
f0ab415a6f
Merge branch 'dev' into feat/make-patched-apps-selectable 2023-09-03 13:49:31 +02:00
Patryk Miś
ecae587491
build: updates (#85) 2023-08-26 14:03:44 +00:00
Ax333l
4edec8afb4
fix(deps): use correct work-runtime version string 2023-08-26 15:52:40 +02:00
Tyff
0e6b4ed350
feat: more info for the select from application screen (#81) 2023-08-23 20:05:21 +02:00
Pun Butrach
12dc8a2585
ci(release): migrate from node12 to node16
This bump `actions/upload-artifact`@v2 to `actions/upload-artifact`@v3
2023-08-19 16:00:01 +07:00
Robert
fe95afca6c
feat: store patched apps (#79)
* feat: store patched apps

* fix: missing string

* feat: save patch selection

* feat: things

* fix: fix broken query

* fix: remove redundant `withContext`

* fix: fix
2023-08-17 17:42:10 +02:00
Palm
0ba1ac6880
ci(release): use correct vars context object
why am i so stupid
2023-08-17 19:30:31 +07:00
Palm
e37cbf19f2
ci(release): no longer store keystore alias in secrets
fixes an issue where GitHub Actions logs would be censored
2023-08-17 19:13:18 +07:00
Ax333l
cd75ac90d9
fix: patches not being reloaded 2023-08-14 18:29:56 +02:00
Ax333l
0520ff23c7
fix: permission error when using installed app 2023-08-12 14:52:34 +02:00
Ax333l
0dc3bed349
feat: patch options UI (#80) 2023-08-12 08:41:22 +00:00
Ax333l
1630668360
feat: switch to the new api (#75) 2023-08-07 09:03:50 +00:00
Ax333l
d666ece4a8
chore: bump patcher 2023-08-04 12:55:14 +02:00
Ax333l
4eca2a3b5d
feat: improve bundle dialog UI 2023-08-04 12:46:07 +02:00
66 changed files with 1558 additions and 517 deletions

View File

@ -34,11 +34,11 @@ jobs:
releaseDirectory: ./app/build/outputs/apk/release/ releaseDirectory: ./app/build/outputs/apk/release/
signingKeyBase64: ${{ secrets.TEMP_SIGNING_KEYSTORE }} signingKeyBase64: ${{ secrets.TEMP_SIGNING_KEYSTORE }}
keyStorePassword: ${{ secrets.TEMP_SIGNING_KEYSTORE_PASSWORD }} keyStorePassword: ${{ secrets.TEMP_SIGNING_KEYSTORE_PASSWORD }}
alias: ${{ secrets.TEMP_SIGNING_KEY_ALIAS }} alias: ${{ vars.TEMP_SIGNING_KEY_ALIAS }}
keyPassword: ${{ secrets.TEMP_SIGNING_KEY_PASSWORD }} keyPassword: ${{ secrets.TEMP_SIGNING_KEY_PASSWORD }}
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: manager name: manager
path: ${{steps.sign_apk.outputs.signedReleaseFile}} path: ${{steps.sign_apk.outputs.signedReleaseFile}}

View File

@ -67,7 +67,7 @@ android {
buildFeatures.compose = true buildFeatures.compose = true
composeOptions.kotlinCompilerExtensionVersion = "1.4.8" composeOptions.kotlinCompilerExtensionVersion = "1.5.1"
} }
kotlin { kotlin {

View File

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "7142188e25ce489eb233aed8fb76e4cc", "identityHash": "5515d164bc8f713201506d42a02d337f",
"entities": [ "entities": [
{ {
"tableName": "patch_bundles", "tableName": "patch_bundles",
@ -190,12 +190,117 @@
}, },
"indices": [], "indices": [],
"foreignKeys": [] "foreignKeys": []
},
{
"tableName": "installed_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`current_package_name` TEXT NOT NULL, `original_package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `install_type` TEXT NOT NULL, PRIMARY KEY(`current_package_name`))",
"fields": [
{
"fieldPath": "currentPackageName",
"columnName": "current_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "originalPackageName",
"columnName": "original_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "installType",
"columnName": "install_type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"current_package_name"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "applied_patch",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bundle",
"columnName": "bundle",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchName",
"columnName": "patch_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"bundle",
"patch_name"
]
},
"indices": [
{
"name": "index_applied_patch_bundle",
"unique": false,
"columnNames": [
"bundle"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_applied_patch_bundle` ON `${TABLE_NAME}` (`bundle`)"
}
],
"foreignKeys": [
{
"table": "installed_app",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"package_name"
],
"referencedColumns": [
"current_package_name"
]
},
{
"table": "patch_bundles",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"bundle"
],
"referencedColumns": [
"uid"
]
}
]
} }
], ],
"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, '7142188e25ce489eb233aed8fb76e4cc')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5515d164bc8f713201506d42a02d337f')"
] ]
} }
} }

View File

@ -26,7 +26,6 @@
android:name=".ManagerApplication" android:name=".ManagerApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:extractNativeLibs="true"
android:largeHeap="true" android:largeHeap="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"

View File

@ -9,6 +9,7 @@ import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.screen.AppInfoScreen
import app.revanced.manager.ui.screen.VersionSelectorScreen import app.revanced.manager.ui.screen.VersionSelectorScreen
import app.revanced.manager.ui.screen.AppSelectorScreen import app.revanced.manager.ui.screen.AppSelectorScreen
import app.revanced.manager.ui.screen.DashboardScreen import app.revanced.manager.ui.screen.DashboardScreen
@ -18,19 +19,14 @@ import app.revanced.manager.ui.screen.SettingsScreen
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 coil.Coil
import coil.ImageLoader
import dev.olshevski.navigation.reimagined.AnimatedNavHost import dev.olshevski.navigation.reimagined.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate import dev.olshevski.navigation.reimagined.navigate
import dev.olshevski.navigation.reimagined.pop import dev.olshevski.navigation.reimagined.pop
import dev.olshevski.navigation.reimagined.popUpTo import dev.olshevski.navigation.reimagined.popUpTo
import dev.olshevski.navigation.reimagined.rememberNavController import dev.olshevski.navigation.reimagined.rememberNavController
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import kotlin.math.roundToInt
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -42,17 +38,6 @@ class MainActivity : ComponentActivity() {
installSplashScreen() installSplashScreen()
val scale = this.resources.displayMetrics.density
val pixels = (36 * scale).roundToInt()
Coil.setImageLoader(
ImageLoader.Builder(this)
.components {
add(AppIconKeyer())
add(AppIconFetcher.Factory(pixels, true, this@MainActivity))
}
.build()
)
setContent { setContent {
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()
@ -77,7 +62,16 @@ class MainActivity : ComponentActivity() {
when (destination) { when (destination) {
is Destination.Dashboard -> DashboardScreen( is Destination.Dashboard -> DashboardScreen(
onSettingsClick = { navController.navigate(Destination.Settings) }, onSettingsClick = { navController.navigate(Destination.Settings) },
onAppSelectorClick = { navController.navigate(Destination.AppSelector) } onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
onAppClick = { installedApp -> navController.navigate(Destination.ApplicationInfo(installedApp)) }
)
is Destination.ApplicationInfo -> AppInfoScreen(
onPatchClick = { packageName, patchesSelection ->
navController.navigate(Destination.VersionSelector(packageName, patchesSelection))
},
onBackClick = { navController.pop() },
viewModel = getViewModel { parametersOf(destination.installedApp) }
) )
is Destination.Settings -> SettingsScreen( is Destination.Settings -> SettingsScreen(
@ -92,8 +86,15 @@ class MainActivity : ComponentActivity() {
is Destination.VersionSelector -> VersionSelectorScreen( is Destination.VersionSelector -> VersionSelectorScreen(
onBackClick = { navController.pop() }, onBackClick = { navController.pop() },
onAppClick = { navController.navigate(Destination.PatchesSelector(it)) }, onAppClick = { selectedApp ->
viewModel = getViewModel { parametersOf(destination.packageName) } navController.navigate(
Destination.PatchesSelector(
selectedApp,
destination.patchesSelection
)
)
},
viewModel = getViewModel { parametersOf(destination.packageName, destination.patchesSelection) }
) )
is Destination.PatchesSelector -> PatchesSelectorScreen( is Destination.PatchesSelector -> PatchesSelectorScreen(
@ -107,7 +108,7 @@ class MainActivity : ComponentActivity() {
) )
) )
}, },
vm = getViewModel { parametersOf(destination.selectedApp) } vm = getViewModel { parametersOf(destination) }
) )
is Destination.Installer -> InstallerScreen( is Destination.Installer -> InstallerScreen(

View File

@ -5,8 +5,12 @@ 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.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import coil.Coil
import coil.ImageLoader
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
@ -36,6 +40,16 @@ class ManagerApplication : Application() {
) )
} }
val pixels = 512
Coil.setImageLoader(
ImageLoader.Builder(this)
.components {
add(AppIconKeyer())
add(AppIconFetcher.Factory(pixels, true, this@ManagerApplication))
}
.build()
)
scope.launch { scope.launch {
prefs.preload() prefs.preload()
} }

View File

@ -3,8 +3,11 @@ package app.revanced.manager.data.room
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import app.revanced.manager.data.room.apps.AppDao import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao
import app.revanced.manager.data.room.apps.DownloadedApp import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.data.room.apps.installed.AppliedPatch
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.data.room.apps.installed.InstalledAppDao
import app.revanced.manager.data.room.selection.PatchSelection import app.revanced.manager.data.room.selection.PatchSelection
import app.revanced.manager.data.room.selection.SelectedPatch import app.revanced.manager.data.room.selection.SelectedPatch
import app.revanced.manager.data.room.selection.SelectionDao import app.revanced.manager.data.room.selection.SelectionDao
@ -12,12 +15,13 @@ import app.revanced.manager.data.room.bundles.PatchBundleDao
import app.revanced.manager.data.room.bundles.PatchBundleEntity import app.revanced.manager.data.room.bundles.PatchBundleEntity
import kotlin.random.Random import kotlin.random.Random
@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class], version = 1) @Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::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
abstract fun selectionDao(): SelectionDao abstract fun selectionDao(): SelectionDao
abstract fun appDao(): AppDao abstract fun downloadedAppDao(): DownloadedAppDao
abstract fun installedAppDao(): InstalledAppDao
companion object { companion object {
fun generateUid() = Random.Default.nextInt() fun generateUid() = Random.Default.nextInt()

View File

@ -1,4 +1,4 @@
package app.revanced.manager.data.room.apps package app.revanced.manager.data.room.apps.downloaded
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity

View File

@ -1,4 +1,4 @@
package app.revanced.manager.data.room.apps package app.revanced.manager.data.room.apps.downloaded
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Delete import androidx.room.Delete
@ -7,7 +7,7 @@ import androidx.room.Query
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface AppDao { interface DownloadedAppDao {
@Query("SELECT * FROM downloaded_app") @Query("SELECT * FROM downloaded_app")
fun getAllApps(): Flow<List<DownloadedApp>> fun getAllApps(): Flow<List<DownloadedApp>>

View File

@ -0,0 +1,34 @@
package app.revanced.manager.data.room.apps.installed
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import app.revanced.manager.data.room.bundles.PatchBundleEntity
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(
tableName = "applied_patch",
primaryKeys = ["package_name", "bundle", "patch_name"],
foreignKeys = [
ForeignKey(
InstalledApp::class,
parentColumns = ["current_package_name"],
childColumns = ["package_name"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
PatchBundleEntity::class,
parentColumns = ["uid"],
childColumns = ["bundle"]
)
],
indices = [Index(value = ["bundle"], unique = false)]
)
data class AppliedPatch(
@ColumnInfo(name = "package_name") val packageName: String,
@ColumnInfo(name = "bundle") val bundle: Int,
@ColumnInfo(name = "patch_name") val patchName: String
) : Parcelable

View File

@ -0,0 +1,23 @@
package app.revanced.manager.data.room.apps.installed
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import app.revanced.manager.R
import kotlinx.parcelize.Parcelize
enum class InstallType(val stringResource: Int) {
DEFAULT(R.string.default_install),
ROOT(R.string.root_install)
}
@Parcelize
@Entity(tableName = "installed_app")
data class InstalledApp(
@PrimaryKey
@ColumnInfo(name = "current_package_name") val currentPackageName: String,
@ColumnInfo(name = "original_package_name") val originalPackageName: String,
@ColumnInfo(name = "version") val version: String,
@ColumnInfo(name = "install_type") val installType: InstallType
) : Parcelable

View File

@ -0,0 +1,40 @@
package app.revanced.manager.data.room.apps.installed
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.MapInfo
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
@Dao
interface InstalledAppDao {
@Query("SELECT * FROM installed_app")
fun getAll(): Flow<List<InstalledApp>>
@Query("SELECT * FROM installed_app WHERE current_package_name = :packageName")
suspend fun get(packageName: String): InstalledApp?
@MapInfo(keyColumn = "bundle", valueColumn = "patch_name")
@Query(
"SELECT bundle, patch_name FROM applied_patch" +
" WHERE package_name = :packageName"
)
suspend fun getPatchesSelection(packageName: String): Map<Int, List<String>>
@Transaction
suspend fun insertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) {
insertApp(installedApp)
insertAppliedPatches(appliedPatches)
}
@Insert
suspend fun insertApp(installedApp: InstalledApp)
@Insert
suspend fun insertAppliedPatches(appliedPatches: List<AppliedPatch>)
@Delete
suspend fun delete(installedApp: InstalledApp)
}

View File

@ -4,14 +4,13 @@ import app.revanced.manager.data.platform.FileSystem
import app.revanced.manager.data.platform.NetworkInfo import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.repository.* import app.revanced.manager.domain.repository.*
import app.revanced.manager.domain.worker.WorkerRepository import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.network.api.ManagerAPI import app.revanced.manager.network.api.ReVancedAPI
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 repositoryModule = module { val repositoryModule = module {
singleOf(::ReVancedRepository) singleOf(::ReVancedAPI)
singleOf(::GithubRepository) singleOf(::GithubRepository)
singleOf(::ManagerAPI)
singleOf(::FileSystem) singleOf(::FileSystem)
singleOf(::NetworkInfo) singleOf(::NetworkInfo)
singleOf(::PatchBundlePersistenceRepository) singleOf(::PatchBundlePersistenceRepository)
@ -19,4 +18,5 @@ val repositoryModule = module {
singleOf(::PatchBundleRepository) singleOf(::PatchBundleRepository)
singleOf(::WorkerRepository) singleOf(::WorkerRepository)
singleOf(::DownloadedAppRepository) singleOf(::DownloadedAppRepository)
singleOf(::InstalledAppRepository)
} }

View File

@ -18,4 +18,6 @@ val viewModelModule = module {
viewModelOf(::ImportExportViewModel) viewModelOf(::ImportExportViewModel)
viewModelOf(::ContributorViewModel) viewModelOf(::ContributorViewModel)
viewModelOf(::DownloadsViewModel) viewModelOf(::DownloadsViewModel)
viewModelOf(::InstalledAppsViewModel)
viewModelOf(::AppInfoViewModel)
} }

View File

@ -2,20 +2,18 @@ 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.data.room.bundles.VersionInfo
import app.revanced.manager.domain.bundles.APIPatchBundle.Companion.toBundleAsset
import app.revanced.manager.domain.repository.Assets
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
import app.revanced.manager.domain.repository.ReVancedRepository import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.Asset import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
import app.revanced.manager.network.dto.BundleAsset import app.revanced.manager.network.dto.BundleAsset
import app.revanced.manager.network.dto.BundleInfo 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.ghIntegrations import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.ghPatches import app.revanced.manager.util.JAR_MIMETYPE
import io.ktor.client.request.url import io.ktor.client.request.url
import io.ktor.http.Url
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -57,7 +55,11 @@ 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(info.patches.version, info.integrations.version) == currentVersion()) { if (hasInstalled() && VersionInfo(
info.patches.version,
info.integrations.version
) == currentVersion()
) {
return@withContext false return@withContext false
} }
@ -94,18 +96,24 @@ class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String)
class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) : class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle(name, id, directory, endpoint) { RemotePatchBundle(name, id, directory, endpoint) {
private val api: ReVancedRepository by inject() private val api: ReVancedAPI by inject()
override suspend fun getLatestInfo() = api.getAssets().toBundleInfo() override suspend fun getLatestInfo() = coroutineScope {
fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) {
private companion object { api
fun Assets.toBundleInfo(): BundleInfo { .getRelease(repo)
val patches = find(ghPatches, ".jar") .getOrThrow()
val integrations = find(ghIntegrations, ".apk") .let {
BundleAsset(it.metadata.tag, it.findAssetByType(mime).downloadUrl)
return BundleInfo(patches.toBundleAsset(), integrations.toBundleAsset()) }
} }
fun Asset.toBundleAsset() = BundleAsset(version, downloadUrl) val patches = getAssetAsync("revanced-patches", JAR_MIMETYPE)
val integrations = getAssetAsync("revanced-integrations", APK_MIMETYPE)
BundleInfo(
patches.await(),
integrations.await()
)
} }
} }

View File

@ -10,7 +10,7 @@ class PreferencesManager(
val dynamicColor = booleanPreference("dynamic_color", true) val dynamicColor = booleanPreference("dynamic_color", true)
val theme = enumPreference("theme", Theme.SYSTEM) val theme = enumPreference("theme", Theme.SYSTEM)
val api = stringPreference("api_url", "https://releases.revanced.app") val api = stringPreference("api_url", "https://api.revanced.app")
val allowExperimental = booleanPreference("allow_experimental", false) val allowExperimental = booleanPreference("allow_experimental", false)

View File

@ -1,14 +1,14 @@
package app.revanced.manager.domain.repository package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.apps.DownloadedApp import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import java.io.File import java.io.File
class DownloadedAppRepository( class DownloadedAppRepository(
db: AppDatabase db: AppDatabase
) { ) {
private val dao = db.appDao() private val dao = db.downloadedAppDao()
fun getAll() = dao.getAllApps().distinctUntilChanged() fun getAll() = dao.getAllApps().distinctUntilChanged()

View File

@ -2,6 +2,7 @@ package app.revanced.manager.domain.repository
import app.revanced.manager.network.service.GithubService import app.revanced.manager.network.service.GithubService
// TODO: delete this when the revanced api adds download count.
class GithubRepository(private val service: GithubService) { class GithubRepository(private val service: GithubService) {
suspend fun getChangelog(repo: String) = service.getChangelog(repo) suspend fun getChangelog(repo: String) = service.getChangelog(repo)
} }

View File

@ -0,0 +1,51 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.apps.installed.AppliedPatch
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.util.PatchesSelection
import kotlinx.coroutines.flow.distinctUntilChanged
class InstalledAppRepository(
db: AppDatabase
) {
private val dao = db.installedAppDao()
fun getAll() = dao.getAll().distinctUntilChanged()
suspend fun get(packageName: String) = dao.get(packageName)
suspend fun getAppliedPatches(packageName: String): PatchesSelection =
dao.getPatchesSelection(packageName).mapValues { (_, patches) -> patches.toSet() }
suspend fun add(
currentPackageName: String,
originalPackageName: String,
version: String,
installType: InstallType,
patchesSelection: PatchesSelection
) {
dao.insertApp(
InstalledApp(
currentPackageName = currentPackageName,
originalPackageName = originalPackageName,
version = version,
installType = installType
),
patchesSelection.flatMap { (uid, patches) ->
patches.map { patch ->
AppliedPatch(
packageName = currentPackageName,
bundle = uid,
patchName = patch
)
}
}
)
}
suspend fun delete(installedApp: InstalledApp) {
dao.delete(installedApp)
}
}

View File

@ -1,25 +0,0 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.network.api.MissingAssetException
import app.revanced.manager.network.dto.Asset
import app.revanced.manager.network.dto.ReVancedReleases
import app.revanced.manager.network.service.ReVancedService
import app.revanced.manager.network.utils.getOrThrow
class ReVancedRepository(
private val service: ReVancedService,
private val prefs: PreferencesManager
) {
private suspend fun apiUrl() = prefs.api.get()
suspend fun getContributors() = service.getContributors(apiUrl())
suspend fun getAssets() = Assets(service.getAssets(apiUrl()).getOrThrow())
}
class Assets(private val releases: ReVancedReleases): List<Asset> by releases.tools {
fun find(repo: String, file: String) = find { asset ->
asset.name.contains(file) && asset.repository.contains(repo)
} ?: throw MissingAssetException()
}

View File

@ -1,43 +0,0 @@
package app.revanced.manager.network.api
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import app.revanced.manager.domain.repository.Assets
import app.revanced.manager.domain.repository.ReVancedRepository
import app.revanced.manager.network.dto.Asset
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.util.*
import io.ktor.client.plugins.onDownload
import io.ktor.client.request.url
import java.io.File
// TODO: merge ReVancedRepository into this class
class ManagerAPI(
private val http: HttpService,
private val revancedRepository: ReVancedRepository
) {
var downloadProgress: Float? by mutableStateOf(null)
var downloadedSize: Long? by mutableStateOf(null)
var totalSize: Long? by mutableStateOf(null)
private suspend fun downloadAsset(asset: Asset, saveLocation: File) {
http.download(saveLocation) {
url(asset.downloadUrl)
onDownload { bytesSentTotal, contentLength ->
downloadProgress = (bytesSentTotal.toFloat() / contentLength.toFloat())
downloadedSize = bytesSentTotal
totalSize = contentLength
}
}
downloadProgress = null
}
suspend fun downloadManager(location: File) {
val managerAsset = revancedRepository.getAssets().find(ghManager, ".apk")
downloadAsset(managerAsset, location)
}
}
class MissingAssetException : Exception()

View File

@ -0,0 +1,26 @@
package app.revanced.manager.network.api
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.network.dto.Asset
import app.revanced.manager.network.dto.ReVancedLatestRelease
import app.revanced.manager.network.dto.ReVancedRelease
import app.revanced.manager.network.service.ReVancedService
import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.network.utils.transform
class ReVancedAPI(
private val service: ReVancedService,
private val prefs: PreferencesManager
) {
private suspend fun apiUrl() = prefs.api.get()
suspend fun getContributors() = service.getContributors(apiUrl()).transform { it.repositories }
suspend fun getRelease(name: String) = service.getRelease(apiUrl(), name).transform { it.release }
companion object Extensions {
fun ReVancedRelease.findAssetByType(mime: String) = assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime)
}
}
class MissingAssetException(type: String) : Exception("No asset with type $type")

View File

@ -4,18 +4,18 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
class ReVancedRepositories( data class ReVancedGitRepositories(
@SerialName("repositories") val repositories: List<ReVancedRepository>, val repositories: List<ReVancedGitRepository>,
) )
@Serializable @Serializable
class ReVancedRepository( data class ReVancedGitRepository(
@SerialName("name") val name: String, val name: String,
@SerialName("contributors") val contributors: List<ReVancedContributor>, val contributors: List<ReVancedContributor>,
) )
@Serializable @Serializable
class ReVancedContributor( data class ReVancedContributor(
@SerialName("login") val username: String, val username: String,
@SerialName("avatar_url") val avatarUrl: String, @SerialName("avatar_url") val avatarUrl: String,
) )

View File

@ -0,0 +1,32 @@
package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ReVancedLatestRelease(
val release: ReVancedRelease,
)
@Serializable
data class ReVancedRelease(
val metadata: ReVancedReleaseMeta,
val assets: List<Asset>
)
@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
)
@Serializable
data class Asset(
val name: String,
@SerialName("browser_download_url") val downloadUrl: String,
@SerialName("content_type") val contentType: String
)

View File

@ -1,20 +0,0 @@
package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class ReVancedReleases(
@SerialName("tools") val tools: List<Asset>,
)
@Serializable
class Asset(
@SerialName("repository") val repository: String,
@SerialName("version") val version: String,
@SerialName("timestamp") val timestamp: String,
@SerialName("name") val name: String,
@SerialName("size") val size: String?,
@SerialName("browser_download_url") val downloadUrl: String,
@SerialName("content_type") val content_type: String
)

View File

@ -1,11 +1,8 @@
package app.revanced.manager.network.service package app.revanced.manager.network.service
import app.revanced.manager.network.api.MissingAssetException import app.revanced.manager.network.dto.ReVancedLatestRelease
import app.revanced.manager.network.dto.Asset import app.revanced.manager.network.dto.ReVancedGitRepositories
import app.revanced.manager.network.dto.ReVancedReleases
import app.revanced.manager.network.dto.ReVancedRepositories
import app.revanced.manager.network.utils.APIResponse import app.revanced.manager.network.utils.APIResponse
import app.revanced.manager.network.utils.getOrThrow
import io.ktor.client.request.* import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -13,20 +10,17 @@ import kotlinx.coroutines.withContext
class ReVancedService( class ReVancedService(
private val client: HttpService, private val client: HttpService,
) { ) {
suspend fun getAssets(api: String): APIResponse<ReVancedReleases> { suspend fun getRelease(api: String, repo: String): APIResponse<ReVancedLatestRelease> =
return withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
client.request { client.request {
url("$api/tools") url("$api/v2/$repo/releases/latest")
} }
} }
}
suspend fun getContributors(api: String): APIResponse<ReVancedRepositories> { suspend fun getContributors(api: String): APIResponse<ReVancedGitRepositories> =
return withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
client.request { client.request {
url("$api/contributors") url("$api/contributors")
} }
} }
}
} }

View File

@ -30,7 +30,7 @@ object Aligning {
} }
file.copyEntriesFromFileAligned( file.copyEntriesFromFileAligned(
ZipFile(inputFile), ZipFile(inputFile, readonly = true),
ZipAligner::getEntryAlignment ZipAligner::getEntryAlignment
) )
} }

View File

@ -28,7 +28,7 @@ class Session(
PatcherOptions( PatcherOptions(
inputFile = input, inputFile = input,
resourceCacheDirectory = temporary.resolve("aapt-resources").path, resourceCacheDirectory = temporary.resolve("aapt-resources").path,
frameworkFolderLocation = frameworkDir, frameworkDirectory = frameworkDir,
aaptPath = aaptPath, aaptPath = aaptPath,
logger = logger, logger = logger,
) )

View File

@ -5,16 +5,17 @@ import app.revanced.manager.patcher.alignment.zip.structures.ZipEntry
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.IOException
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
import java.util.zip.CRC32 import java.util.zip.CRC32
import java.util.zip.Deflater import java.util.zip.Deflater
class ZipFile(file: File) : Closeable { class ZipFile(file: File, private val readonly: Boolean = false) : Closeable {
var entries: MutableList<ZipEntry> = mutableListOf() var entries: MutableList<ZipEntry> = mutableListOf()
private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw") private val filePointer: RandomAccessFile = RandomAccessFile(file, if (readonly) "r" else "rw")
private var CDNeedsRewrite = false private var CDNeedsRewrite = false
private val compressionLevel = 5 private val compressionLevel = 5
@ -34,6 +35,10 @@ class ZipFile(file: File) : Closeable {
filePointer.seek(0) filePointer.seek(0)
} }
private fun assertWritable() {
if (readonly) throw IOException("Archive is read-only")
}
private fun findEndRecord(): ZipEndRecord { private fun findEndRecord(): ZipEndRecord {
//look from end to start since end record is at the end //look from end to start since end record is at the end
for (i in filePointer.length() - 1 downTo 0) { for (i in filePointer.length() - 1 downTo 0) {
@ -110,6 +115,8 @@ class ZipFile(file: File) : Closeable {
} }
fun addEntryCompressData(entry: ZipEntry, data: ByteArray) { fun addEntryCompressData(entry: ZipEntry, data: ByteArray) {
assertWritable()
val compressor = Deflater(compressionLevel, true) val compressor = Deflater(compressionLevel, true)
compressor.setInput(data) compressor.setInput(data)
compressor.finish() compressor.finish()
@ -136,6 +143,8 @@ class ZipFile(file: File) : Closeable {
} }
private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) { private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
assertWritable()
alignment?.let { alignment?.let {
//calculate where data would end up //calculate where data would end up
val dataOffset = filePointer.filePointer + entry.LFHSize val dataOffset = filePointer.filePointer + entry.LFHSize
@ -162,6 +171,8 @@ class ZipFile(file: File) : Closeable {
} }
fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) { fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
assertWritable()
for (entry in file.entries) { for (entry in file.entries) {
if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates

View File

@ -12,14 +12,15 @@ import java.io.File
class PatchBundle(private val loader: Iterable<PatchClass>, val integrations: File?) { class PatchBundle(private val loader: Iterable<PatchClass>, val integrations: File?) {
constructor(bundleJar: File, integrations: File?) : this( constructor(bundleJar: File, integrations: File?) : this(
object : Iterable<PatchClass> { object : Iterable<PatchClass> {
private val bundle = bundleJar.absolutePath.let { private fun load(): List<PatchClass> {
PatchBundle.Dex( val path = bundleJar.absolutePath
it, return PatchBundle.Dex(
PathClassLoader(it, Patcher::class.java.classLoader) path,
) PathClassLoader(path, Patcher::class.java.classLoader)
).loadPatches()
} }
override fun iterator() = bundle.loadPatches().iterator() override fun iterator() = load().iterator()
}, },
integrations integrations
) { ) {

View File

@ -14,7 +14,10 @@ class UninstallService : Service() {
flags: Int, flags: Int,
startId: Int startId: Int
): Int { ): Int {
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) { val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
when (extraStatus) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> { PackageInstaller.STATUS_PENDING_USER_ACTION -> {
startActivity(if (Build.VERSION.SDK_INT >= 33) { startActivity(if (Build.VERSION.SDK_INT >= 33) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
@ -28,6 +31,9 @@ class UninstallService : Service() {
else -> { else -> {
sendBroadcast(Intent().apply { sendBroadcast(Intent().apply {
action = APP_UNINSTALL_ACTION action = APP_UNINSTALL_ACTION
putExtra(EXTRA_UNINSTALL_STATUS, extraStatus)
putExtra(EXTRA_UNINSTALL_STATUS_MESSAGE, extraStatusMessage)
}) })
} }
} }
@ -39,6 +45,9 @@ class UninstallService : Service() {
companion object { companion object {
const val APP_UNINSTALL_ACTION = "APP_UNINSTALL_ACTION" const val APP_UNINSTALL_ACTION = "APP_UNINSTALL_ACTION"
const val EXTRA_UNINSTALL_STATUS = "EXTRA_UNINSTALL_STATUS"
const val EXTRA_UNINSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
} }
} }

View File

@ -25,7 +25,7 @@ fun AppLabel(
packageInfo: PackageInfo?, packageInfo: PackageInfo?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current, style: TextStyle = LocalTextStyle.current,
defaultText: String = stringResource(R.string.not_installed) defaultText: String? = stringResource(R.string.not_installed)
) { ) {
val context = LocalContext.current val context = LocalContext.current

View File

@ -36,6 +36,13 @@ fun AppScaffold(
fun AppTopBar( fun AppTopBar(
title: String, title: String,
onBackClick: (() -> Unit)? = null, onBackClick: (() -> Unit)? = null,
backIcon: @Composable (() -> Unit) = @Composable {
Icon(
imageVector = Icons.Default.ArrowBack, contentDescription = stringResource(
R.string.back
)
)
},
actions: @Composable (RowScope.() -> Unit) = {}, actions: @Composable (RowScope.() -> Unit) = {},
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
@ -47,10 +54,7 @@ fun AppTopBar(
navigationIcon = { navigationIcon = {
if (onBackClick != null) { if (onBackClick != null) {
IconButton(onClick = onBackClick) { IconButton(onClick = onBackClick) {
Icon( backIcon()
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.back)
)
} }
} }
}, },

View File

@ -12,18 +12,26 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
fun LoadingIndicator(progress: Float? = null, text: String? = null) { fun LoadingIndicator(
modifier: Modifier = Modifier,
progress: Float? = null,
text: String? = null
) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (text != null) text?.let { Text(text) }
Text(text)
if (progress == null) { progress?.let {
CircularProgressIndicator(modifier = Modifier.padding(vertical = 16.dp)) CircularProgressIndicator(
} else { progress = progress,
CircularProgressIndicator(progress = progress, modifier = Modifier.padding(vertical = 16.dp)) modifier = Modifier.padding(vertical = 16.dp).then(modifier)
} )
} ?:
CircularProgressIndicator(
modifier = Modifier.padding(vertical = 16.dp).then(modifier)
)
} }
} }

View File

@ -0,0 +1,68 @@
package app.revanced.manager.ui.component
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
/**
* Credits to [Vendetta](https://github.com/vendetta-mod)
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.SegmentedButton(
icon: Any,
iconDescription: String? = null,
text: String,
onClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
modifier = Modifier
.clickable(onClick = onClick)
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp))
.weight(1f)
.padding(vertical = 20.dp)
) {
when (icon) {
is ImageVector -> {
Icon(
imageVector = icon,
contentDescription = iconDescription,
tint = MaterialTheme.colorScheme.primary
)
}
is Painter -> {
Icon(
painter = icon,
contentDescription = iconDescription,
tint = MaterialTheme.colorScheme.primary
)
}
}
Text(
text = text,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
modifier = Modifier.basicMarquee()
)
}
}

View File

@ -0,0 +1,51 @@
package app.revanced.manager.ui.component
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
@Composable
fun TextInputDialog(
initial: String,
title: String,
onDismissRequest: () -> Unit,
onConfirm: (String) -> Unit,
validator: (String) -> Boolean = String::isNotEmpty,
) {
val (value, setValue) = rememberSaveable(initial) {
mutableStateOf(initial)
}
val valid = remember(value, validator) {
validator(value)
}
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
onClick = { onConfirm(value) },
enabled = valid
) {
Text(stringResource(R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.cancel))
}
},
title = {
Text(title)
},
text = {
TextField(value = value, onValueChange = setValue)
}
)
}

View File

@ -1,5 +1,7 @@
package app.revanced.manager.ui.component.bundle package app.revanced.manager.ui.component.bundle
import android.webkit.URLUtil
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -12,23 +14,27 @@ import androidx.compose.material3.FilledTonalButton
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.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.TextInputDialog
@Composable @Composable
fun BaseBundleDialog( fun BaseBundleDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isDefault: Boolean, isDefault: Boolean,
name: String, name: String,
onNameChange: (String) -> Unit = {}, onNameChange: ((String) -> Unit)? = null,
remoteUrl: String?, remoteUrl: String?,
onRemoteUrlChange: (String) -> Unit = {}, onRemoteUrlChange: ((String) -> Unit)? = null,
patchCount: Int, patchCount: Int,
version: String?, version: String?,
autoUpdate: Boolean, autoUpdate: Boolean,
@ -40,79 +46,108 @@ fun BaseBundleDialog(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.then(modifier) .padding(
) {
Column(
modifier = Modifier.padding(
start = 24.dp,
top = 16.dp,
end = 24.dp,
)
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
value = name,
onValueChange = onNameChange,
label = {
Text(stringResource(R.string.bundle_input_name))
}
)
remoteUrl?.takeUnless { isDefault }?.let {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
value = it,
onValueChange = onRemoteUrlChange,
label = {
Text(stringResource(R.string.bundle_input_source_url))
}
)
}
extraFields()
}
Column(
Modifier.padding(
start = 8.dp, start = 8.dp,
top = 8.dp, top = 8.dp,
end = 4.dp, end = 4.dp,
) )
) Info@{ .then(modifier)
if (remoteUrl != null) { ) {
BundleListItem( var showNameInputDialog by rememberSaveable {
headlineText = stringResource(R.string.automatically_update), mutableStateOf(false)
supportingText = stringResource(R.string.automatically_update_description), }
trailingContent = { if (showNameInputDialog) {
Switch( TextInputDialog(
checked = autoUpdate, initial = name,
onCheckedChange = onAutoUpdateChange title = stringResource(R.string.bundle_input_name),
) onDismissRequest = {
showNameInputDialog = false
},
onConfirm = {
showNameInputDialog = false
onNameChange?.invoke(it)
},
validator = {
it.length in 1..19
}
)
}
BundleListItem(
headlineText = stringResource(R.string.bundle_input_name),
supportingText = name.ifEmpty { stringResource(R.string.field_not_set) },
modifier = Modifier.clickable(enabled = onNameChange != null) {
showNameInputDialog = true
}
)
remoteUrl?.takeUnless { isDefault }?.let { url ->
var showUrlInputDialog by rememberSaveable {
mutableStateOf(false)
}
if (showUrlInputDialog) {
TextInputDialog(
initial = url,
title = stringResource(R.string.bundle_input_source_url),
onDismissRequest = { showUrlInputDialog = false },
onConfirm = {
showUrlInputDialog = false
onRemoteUrlChange?.invoke(it)
},
validator = {
if (it.isEmpty()) return@TextInputDialog false
URLUtil.isValidUrl(it)
} }
) )
} }
BundleListItem( BundleListItem(
headlineText = stringResource(R.string.bundle_type), modifier = Modifier.clickable(enabled = onRemoteUrlChange != null) {
supportingText = stringResource(R.string.bundle_type_description) showUrlInputDialog = true
) { },
FilledTonalButton( headlineText = stringResource(R.string.bundle_input_source_url),
onClick = onBundleTypeClick, supportingText = url.ifEmpty { stringResource(R.string.field_not_set) }
content = { )
if (remoteUrl == null) { }
Text(stringResource(R.string.local))
} else { extraFields()
Text(stringResource(R.string.remote))
} if (remoteUrl != null) {
} BundleListItem(
) headlineText = stringResource(R.string.automatically_update),
supportingText = stringResource(R.string.automatically_update_description),
trailingContent = {
Switch(
checked = autoUpdate,
onCheckedChange = onAutoUpdateChange
)
},
modifier = Modifier.clickable {
onAutoUpdateChange(!autoUpdate)
}
)
}
BundleListItem(
headlineText = stringResource(R.string.bundle_type),
supportingText = stringResource(R.string.bundle_type_description),
modifier = Modifier.clickable {
onBundleTypeClick()
} }
) {
FilledTonalButton(
onClick = onBundleTypeClick,
content = {
if (remoteUrl == null) {
Text(stringResource(R.string.local))
} else {
Text(stringResource(R.string.remote))
}
}
)
}
if (version == null && patchCount < 1) return@Info if (version != null || patchCount > 0) {
Text( Text(
text = stringResource(R.string.information), text = stringResource(R.string.information),
modifier = Modifier.padding( modifier = Modifier.padding(
@ -122,7 +157,9 @@ fun BaseBundleDialog(
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
}
if (patchCount > 0) {
BundleListItem( BundleListItem(
headlineText = stringResource(R.string.patches), headlineText = stringResource(R.string.patches),
supportingText = if (patchCount == 0) stringResource(R.string.no_patches) supportingText = if (patchCount == 0) stringResource(R.string.no_patches)
@ -138,12 +175,12 @@ fun BaseBundleDialog(
} }
} }
) )
}
if (version == null) return@Info version?.let {
BundleListItem( BundleListItem(
headlineText = stringResource(R.string.version), headlineText = stringResource(R.string.version),
supportingText = version, supportingText = it,
) )
} }
} }

View File

@ -67,7 +67,7 @@ fun BundleInformationDialog(
Scaffold( Scaffold(
topBar = { topBar = {
BundleTopBar( BundleTopBar(
title = stringResource(R.string.bundle_information), title = bundle.name,
onBackClick = onDismissRequest, onBackClick = onDismissRequest,
onBackIcon = { onBackIcon = {
Icon( Icon(

View File

@ -4,9 +4,11 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable @Composable
fun BundleListItem( fun BundleListItem(
modifier: Modifier = Modifier,
headlineText: String, headlineText: String,
supportingText: String = "", supportingText: String = "",
trailingContent: @Composable (() -> Unit)? = null, trailingContent: @Composable (() -> Unit)? = null,
@ -26,5 +28,6 @@ fun BundleListItem(
) )
}, },
trailingContent = trailingContent, trailingContent = trailingContent,
modifier = modifier
) )
} }

View File

@ -1,10 +1,9 @@
package app.revanced.manager.ui.component.bundle package app.revanced.manager.ui.component.bundle
import android.net.Uri import android.net.Uri
import android.webkit.URLUtil
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.layout.fillMaxWidth import androidx.compose.foundation.clickable
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.Close import androidx.compose.material.icons.filled.Close
@ -12,7 +11,6 @@ import androidx.compose.material.icons.filled.Topic
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.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -46,17 +44,9 @@ fun ImportBundleDialog(
var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) } var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
var integrations by rememberSaveable { mutableStateOf<Uri?>(null) } var integrations by rememberSaveable { mutableStateOf<Uri?>(null) }
val patchBundleText = patchBundle?.toString().orEmpty()
val integrationText = integrations?.toString().orEmpty()
val inputsAreValid by remember { val inputsAreValid by remember {
derivedStateOf { derivedStateOf {
val nameSize = name.length name.isNotEmpty() && if (isLocal) patchBundle != null else remoteUrl.isNotEmpty()
when {
nameSize !in 1..19 -> false
isLocal -> patchBundle != null
else -> remoteUrl.isNotEmpty() && URLUtil.isValidUrl(remoteUrl)
}
} }
} }
@ -64,10 +54,17 @@ fun ImportBundleDialog(
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { patchBundle = it } uri?.let { patchBundle = it }
} }
fun launchPatchActivity() {
patchActivityLauncher.launch(JAR_MIMETYPE)
}
val integrationsActivityLauncher = val integrationsActivityLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { integrations = it } uri?.let { integrations = it }
} }
fun launchIntegrationsActivity() {
integrationsActivityLauncher.launch(APK_MIMETYPE)
}
Dialog( Dialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -123,53 +120,43 @@ fun ImportBundleDialog(
onPatchesClick = {}, onPatchesClick = {},
onBundleTypeClick = { isLocal = !isLocal }, onBundleTypeClick = { isLocal = !isLocal },
) { ) {
if (isLocal) { if (!isLocal) return@BaseBundleDialog
OutlinedTextField(
modifier = Modifier BundleListItem(
.fillMaxWidth() headlineText = stringResource(R.string.patch_bundle_field),
.padding(bottom = 16.dp), supportingText = stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set),
value = patchBundleText, trailingContent = {
onValueChange = {}, IconButton(
label = { onClick = ::launchPatchActivity
Text("Patches Source File") ) {
}, Icon(
trailingIcon = { imageVector = Icons.Default.Topic,
IconButton( contentDescription = null
onClick = { )
patchActivityLauncher.launch(JAR_MIMETYPE)
}
) {
Icon(
imageVector = Icons.Default.Topic,
contentDescription = null
)
}
} }
) },
modifier = Modifier.clickable {
launchPatchActivity()
}
)
OutlinedTextField( BundleListItem(
modifier = Modifier headlineText = stringResource(R.string.integrations_field),
.fillMaxWidth() supportingText = stringResource(if (integrations != null) R.string.file_field_set else R.string.file_field_not_set),
.padding(bottom = 16.dp), trailingContent = {
value = integrationText, IconButton(
onValueChange = {}, onClick = ::launchIntegrationsActivity
label = { ) {
Text("Integrations Source File") Icon(
}, imageVector = Icons.Default.Topic,
trailingIcon = { contentDescription = null
IconButton( )
onClick = {
integrationsActivityLauncher.launch(APK_MIMETYPE)
}
) {
Icon(
imageVector = Icons.Default.Topic,
contentDescription = null
)
}
} }
) },
} modifier = Modifier.clickable {
launchIntegrationsActivity()
}
)
} }
} }
} }

View File

@ -1,38 +1,71 @@
package app.revanced.manager.ui.component.patches package app.revanced.manager.ui.component.patches
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.Button import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.ListItem
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.util.toast
import app.revanced.patcher.patch.PatchOption import app.revanced.patcher.patch.PatchOption
import org.koin.compose.rememberKoinInject import org.koin.compose.rememberKoinInject
/** // Composable functions do not support function references, so we have to use composable lambdas instead.
* [Composable] functions do not support function references, so we have to use composable lambdas instead. private typealias OptionImpl = @Composable (Option, Any?, (Any?) -> Unit) -> Unit
*/
private typealias OptionField = @Composable (Any?, (Any?) -> Unit) -> Unit
private val StringField: OptionField = { value, setValue -> @Composable
val fs: FileSystem = rememberKoinInject() private fun OptionListItem(
option: Option,
onClick: () -> Unit,
trailingContent: @Composable () -> Unit
) {
ListItem(
modifier = Modifier.clickable(onClick = onClick),
headlineContent = { Text(option.title) },
supportingContent = { Text(option.description) },
trailingContent = trailingContent
)
}
@Composable
private fun StringOptionDialog(
name: String,
value: String?,
onSubmit: (String) -> Unit,
onDismissRequest: () -> Unit
) {
var showFileDialog by rememberSaveable { mutableStateOf(false) } var showFileDialog by rememberSaveable { mutableStateOf(false) }
var fieldValue by rememberSaveable(value) {
mutableStateOf(value.orEmpty())
}
val fs: FileSystem = rememberKoinInject()
val (contract, permissionName) = fs.permissionContract() val (contract, permissionName) = fs.permissionContract()
val permissionLauncher = rememberLauncherForActivityResult(contract = contract) { val permissionLauncher = rememberLauncherForActivityResult(contract = contract) {
showFileDialog = it showFileDialog = it
} }
val current = value as? String
if (showFileDialog) { if (showFileDialog) {
PathSelectorDialog( PathSelectorDialog(
@ -40,45 +73,133 @@ private val StringField: OptionField = { value, setValue ->
) { ) {
showFileDialog = false showFileDialog = false
it?.let { path -> it?.let { path ->
setValue(path.toString()) fieldValue = path.toString()
} }
} }
} }
Column { AlertDialog(
TextField(value = current ?: "", onValueChange = setValue) onDismissRequest = onDismissRequest,
Button(onClick = { title = { Text(name) },
if (fs.hasStoragePermission()) { text = {
showFileDialog = true OutlinedTextField(
} else { value = fieldValue,
permissionLauncher.launch(permissionName) onValueChange = { fieldValue = it },
placeholder = {
Text(stringResource(R.string.string_option_placeholder))
},
trailingIcon = {
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
IconButton(
onClick = { showDropdownMenu = true }
) {
Icon(
Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.string_option_menu_description)
)
}
DropdownMenu(
expanded = showDropdownMenu,
onDismissRequest = { showDropdownMenu = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Outlined.Folder, null)
},
text = {
Text(stringResource(R.string.path_selector))
},
onClick = {
showDropdownMenu = false
if (fs.hasStoragePermission()) {
showFileDialog = true
} else {
permissionLauncher.launch(permissionName)
}
}
)
}
}
)
},
confirmButton = {
TextButton(onClick = { onSubmit(fieldValue) }) {
Text(stringResource(R.string.save))
} }
}) { },
Icon(Icons.Filled.FileOpen, null) dismissButton = {
Text("Select file or folder") TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.cancel))
}
},
)
}
private val StringOption: OptionImpl = { option, value, setValue ->
var showInputDialog by rememberSaveable { mutableStateOf(false) }
fun showInputDialog() {
showInputDialog = true
}
fun dismissInputDialog() {
showInputDialog = false
}
if (showInputDialog) {
StringOptionDialog(
name = option.title,
value = value as? String,
onSubmit = {
dismissInputDialog()
setValue(it)
},
onDismissRequest = ::dismissInputDialog
)
}
OptionListItem(
option = option,
onClick = ::showInputDialog
) {
IconButton(onClick = ::showInputDialog) {
Icon(
Icons.Outlined.Edit,
contentDescription = stringResource(R.string.string_option_icon_description)
)
} }
} }
} }
private val BooleanField: OptionField = { value, setValue -> private val BooleanOption: OptionImpl = { option, value, setValue ->
val current = value as? Boolean val current = (value as? Boolean) ?: false
Switch(checked = current ?: false, onCheckedChange = setValue)
OptionListItem(
option = option,
onClick = { setValue(!current) }
) {
Switch(checked = current, onCheckedChange = setValue)
}
} }
private val UnknownField: OptionField = { _, _ -> private val UnknownOption: OptionImpl = { option, _, _ ->
Text("This type has not been implemented") val context = LocalContext.current
OptionListItem(
option = option,
onClick = { context.toast("Unknown type: ${option.type.name}") },
trailingContent = {})
} }
@Composable @Composable
fun OptionField(option: Option, value: Any?, setValue: (Any?) -> Unit) { fun OptionItem(option: Option, value: Any?, setValue: (Any?) -> Unit) {
val implementation = remember(option.type) { val implementation = remember(option.type) {
when (option.type) { when (option.type) {
// These are the only two types that are currently used by the official patches. // These are the only two types that are currently used by the official patches.
PatchOption.StringOption::class.java -> StringField PatchOption.StringOption::class.java -> StringOption
PatchOption.BooleanOption::class.java -> BooleanField PatchOption.BooleanOption::class.java -> BooleanOption
else -> UnknownField else -> UnknownOption
} }
} }
implementation(value, setValue) implementation(option, value, setValue)
} }

View File

@ -2,18 +2,20 @@ package app.revanced.manager.ui.component.patches
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.DocumentScanner
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.InsertDriveFile
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ListItem
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
@ -21,15 +23,18 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
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
import app.revanced.manager.R 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.GroupHeader
import app.revanced.manager.util.saver.PathSaver import app.revanced.manager.util.saver.PathSaver
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.isDirectory import kotlin.io.path.isDirectory
import kotlin.io.path.isRegularFile import kotlin.io.path.isReadable
import kotlin.io.path.listDirectoryEntries import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.name import kotlin.io.path.name
@ -40,14 +45,8 @@ fun PathSelectorDialog(root: Path, onSelect: (Path?) -> Unit) {
val notAtRootDir = remember(currentDirectory) { val notAtRootDir = remember(currentDirectory) {
currentDirectory != root currentDirectory != root
} }
val everything = remember(currentDirectory) { val (directories, files) = remember(currentDirectory) {
currentDirectory.listDirectoryEntries() currentDirectory.listDirectoryEntries().filter(Path::isReadable).partition(Path::isDirectory)
}
val directories = remember(everything) {
everything.filter { it.isDirectory() }
}
val files = remember(everything) {
everything.filter { it.isRegularFile() }
} }
Dialog( Dialog(
@ -60,51 +59,78 @@ fun PathSelectorDialog(root: Path, onSelect: (Path?) -> Unit) {
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.select_file), title = stringResource(R.string.path_selector),
onBackClick = { onSelect(null) } onBackClick = { onSelect(null) },
backIcon = {
Icon(Icons.Filled.Close, contentDescription = stringResource(R.string.close))
}
) )
} },
) { paddingValues -> ) { paddingValues ->
BackHandler(enabled = notAtRootDir) { BackHandler(enabled = notAtRootDir) {
currentDirectory = currentDirectory.parent currentDirectory = currentDirectory.parent
} }
Column( LazyColumn(
modifier = Modifier modifier = Modifier.padding(paddingValues)
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) { ) {
Text(text = currentDirectory.toString()) item(key = "current") {
Row( PathItem(
modifier = Modifier.clickable { onSelect(currentDirectory) } onClick = { onSelect(currentDirectory) },
) { icon = Icons.Outlined.Folder,
Text("(Use this directory)") name = currentDirectory.toString()
)
} }
if (notAtRootDir) { if (notAtRootDir) {
Row( item(key = "parent") {
modifier = Modifier.clickable { currentDirectory = currentDirectory.parent } PathItem(
) { onClick = { currentDirectory = currentDirectory.parent },
Text("Previous directory") icon = Icons.Outlined.ArrowBack,
name = stringResource(R.string.path_selector_parent_dir)
)
} }
} }
directories.forEach { if (directories.isNotEmpty()) {
Row( item(key = "dirs_header") {
modifier = Modifier.clickable { currentDirectory = it } GroupHeader(title = stringResource(R.string.path_selector_dirs))
) {
Icon(Icons.Filled.Folder, null)
Text(text = it.name)
} }
} }
files.forEach { items(directories, key = { it.absolutePathString() }) {
Row( PathItem(
modifier = Modifier.clickable { onSelect(it) } onClick = { currentDirectory = it },
) { icon = Icons.Outlined.Folder,
Icon(Icons.Filled.FileOpen, null) name = it.name
Text(text = it.name) )
}
if (files.isNotEmpty()) {
item(key = "files_header") {
GroupHeader(title = stringResource(R.string.path_selector_files))
} }
} }
items(files, key = { it.absolutePathString() }) {
PathItem(
onClick = { onSelect(it) },
icon = Icons.Outlined.InsertDriveFile,
name = it.name
)
}
} }
} }
} }
}
@Composable
private fun PathItem(
onClick: () -> Unit,
icon: ImageVector,
name: String
) {
ListItem(
modifier = Modifier.clickable(onClick = onClick),
headlineContent = { Text(name) },
leadingContent = { Icon(icon, contentDescription = null) }
)
} }

View File

@ -1,6 +1,7 @@
package app.revanced.manager.ui.destination package app.revanced.manager.ui.destination
import android.os.Parcelable import android.os.Parcelable
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
@ -12,6 +13,9 @@ sealed interface Destination : Parcelable {
@Parcelize @Parcelize
object Dashboard : Destination object Dashboard : Destination
@Parcelize
data class ApplicationInfo(val installedApp: InstalledApp) : Destination
@Parcelize @Parcelize
object AppSelector : Destination object AppSelector : Destination
@ -19,11 +23,12 @@ sealed interface Destination : Parcelable {
object Settings : Destination object Settings : Destination
@Parcelize @Parcelize
data class VersionSelector(val packageName: String) : Destination data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination
@Parcelize @Parcelize
data class PatchesSelector(val selectedApp: SelectedApp) : Destination data class PatchesSelector(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
@Parcelize @Parcelize
data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination
} }

View File

@ -0,0 +1,158 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
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.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowRight
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.ExperimentalMaterial3Api
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.SideEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppIcon
import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.SegmentedButton
import app.revanced.manager.ui.viewmodel.AppInfoViewModel
import app.revanced.manager.util.PatchesSelection
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppInfoScreen(
onPatchClick: (packageName: String, patchesSelection: PatchesSelection) -> Unit,
onBackClick: () -> Unit,
viewModel: AppInfoViewModel
) {
SideEffect {
viewModel.onBackClick = onBackClick
}
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.app_info),
onBackClick = onBackClick
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AppIcon(
viewModel.appInfo,
contentDescription = null,
modifier = Modifier
.size(100.dp)
.padding(bottom = 5.dp)
)
AppLabel(
viewModel.appInfo,
style = MaterialTheme.typography.titleLarge,
defaultText = null
)
Text(viewModel.installedApp.version, style = MaterialTheme.typography.bodySmall)
}
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(24.dp))
) {
SegmentedButton(
icon = Icons.Outlined.OpenInNew,
text = stringResource(R.string.open_app),
onClick = viewModel::launch
)
SegmentedButton(
icon = Icons.Outlined.Delete,
text = stringResource(R.string.uninstall),
onClick = viewModel::uninstall
)
SegmentedButton(
icon = Icons.Outlined.Update,
text = stringResource(R.string.repatch),
onClick = {
viewModel.appliedPatches?.let {
onPatchClick(viewModel.installedApp.originalPackageName, it)
}
}
)
}
Column(
modifier = Modifier.padding(vertical = 16.dp)
) {
ListItem(
modifier = Modifier.clickable { },
headlineContent = { Text(stringResource(R.string.applied_patches)) },
supportingContent = {
Text(
(viewModel.appliedPatches?.values?.sumOf { it.size } ?: 0).let {
pluralStringResource(
id = R.plurals.applied_patches,
it,
it
)
}
)
},
trailingContent = { Icon(Icons.Filled.ArrowRight, contentDescription = stringResource(R.string.view_applied_patches)) }
)
ListItem(
headlineContent = { Text(stringResource(R.string.package_name)) },
supportingContent = { Text(viewModel.installedApp.currentPackageName) }
)
if (viewModel.installedApp.originalPackageName != viewModel.installedApp.currentPackageName) {
ListItem(
headlineContent = { Text(stringResource(R.string.original_package_name)) },
supportingContent = { Text(viewModel.installedApp.originalPackageName) }
)
}
ListItem(
headlineContent = { Text(stringResource(R.string.install_type)) },
supportingContent = { Text(stringResource(viewModel.installedApp.installType.stringResource)) }
)
}
}
}
}

View File

@ -49,7 +49,11 @@ fun AppSelectorScreen(
val pickApkLauncher = val pickApkLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { apkUri -> uri?.let { apkUri ->
vm.loadSelectedFile(apkUri)?.let(onStorageClick) ?: context.toast(context.getString(R.string.failed_to_load_apk)) vm.loadSelectedFile(apkUri)?.let(onStorageClick) ?: context.toast(
context.getString(
R.string.failed_to_load_apk
)
)
} }
} }
@ -85,28 +89,62 @@ fun AppSelectorScreen(
} }
}, },
content = { content = {
LazyColumn(
modifier = Modifier.fillMaxSize() if (appList.isNotEmpty() && filterText.isNotEmpty()) {
) {
if (appList.isNotEmpty()) { LazyColumn(
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 { onAppClick(app.packageName) },
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) }, leadingContent = {
AppIcon(
app.packageInfo,
null,
Modifier.size(36.dp)
)
},
headlineContent = { AppLabel(app.packageInfo) }, headlineContent = { AppLabel(app.packageInfo) },
supportingContent = { Text(app.packageName) }, supportingContent = { Text(app.packageName) },
trailingContent = app.patches?.let { { Text(pluralStringResource(R.plurals.patches_count, it, it)) } } trailingContent = app.patches?.let {
{
Text(
pluralStringResource(
R.plurals.patches_count,
it,
it
)
)
}
}
) )
} }
} else { }
item { LoadingIndicator() } } else {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Outlined.Search,
contentDescription = stringResource(R.string.search),
modifier = Modifier.size(64.dp)
)
Text(
text = stringResource(R.string.type_anything),
style = MaterialTheme.typography.bodyLarge
)
} }
} }
} }
) )
} }
@ -146,7 +184,10 @@ fun AppSelectorScreen(
) )
} }
}, },
headlineContent = { Text(stringResource(R.string.select_from_storage)) } headlineContent = { Text(stringResource(R.string.select_from_storage)) },
supportingContent = {
Text(stringResource(R.string.select_from_storage_description))
}
) )
Divider() Divider()
} }
@ -162,7 +203,17 @@ fun AppSelectorScreen(
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) }, leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
headlineContent = { AppLabel(app.packageInfo) }, headlineContent = { AppLabel(app.packageInfo) },
supportingContent = { Text(app.packageName) }, supportingContent = { Text(app.packageName) },
trailingContent = app.patches?.let { { Text(pluralStringResource(R.plurals.patches_count, it, it)) } } trailingContent = app.patches?.let {
{
Text(
pluralStringResource(
R.plurals.patches_count,
it,
it
)
)
}
}
) )
} }

View File

@ -15,16 +15,7 @@ import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Source import androidx.compose.material.icons.outlined.Source
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.*
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
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
@ -41,6 +32,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.Companion.isDefault import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.bundle.BundleItem import app.revanced.manager.ui.component.bundle.BundleItem
import app.revanced.manager.ui.component.bundle.BundleTopBar import app.revanced.manager.ui.component.bundle.BundleTopBar
@ -64,6 +56,7 @@ fun DashboardScreen(
vm: DashboardViewModel = getViewModel(), vm: DashboardViewModel = getViewModel(),
onAppSelectorClick: () -> Unit, onAppSelectorClick: () -> Unit,
onSettingsClick: () -> Unit, onSettingsClick: () -> Unit,
onAppClick: (InstalledApp) -> Unit
) { ) {
var showImportBundleDialog by rememberSaveable { mutableStateOf(false) } var showImportBundleDialog by rememberSaveable { mutableStateOf(false) }
@ -195,7 +188,9 @@ fun DashboardScreen(
pageContent = { index -> pageContent = { index ->
when (pages[index]) { when (pages[index]) {
DashboardPage.DASHBOARD -> { DashboardPage.DASHBOARD -> {
InstalledAppsScreen() InstalledAppsScreen(
onAppClick = onAppClick
)
} }
DashboardPage.BUNDLES -> { DashboardPage.BUNDLES -> {

View File

@ -1,21 +1,73 @@
package app.revanced.manager.ui.screen package app.revanced.manager.ui.screen
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
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.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.ui.component.AppIcon
import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.viewmodel.InstalledAppsViewModel
import org.koin.androidx.compose.getViewModel
@Composable @Composable
fun InstalledAppsScreen() { fun InstalledAppsScreen(
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { onAppClick: (InstalledApp) -> Unit,
Text( viewModel: InstalledAppsViewModel = getViewModel()
text = stringResource(R.string.no_patched_apps_found), ) {
style = MaterialTheme.typography.titleLarge val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null)
)
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = installedApps?.let { if (it.isEmpty()) Arrangement.Center else Arrangement.Top } ?: Arrangement.Center
) {
installedApps?.let { installedApps ->
if (installedApps.isNotEmpty()) {
items(
installedApps,
key = { it.currentPackageName }
) { installedApp ->
viewModel.packageInfoMap[installedApp.currentPackageName].let { packageInfo ->
ListItem(
modifier = Modifier.clickable { onAppClick(installedApp) },
leadingContent = {
AppIcon(
packageInfo,
contentDescription = null,
Modifier.size(36.dp)
)
},
headlineContent = { AppLabel(packageInfo, defaultText = null) },
supportingContent = { Text(installedApp.currentPackageName) }
)
}
}
} else {
item {
Text(
text = stringResource(R.string.no_patched_apps_found),
style = MaterialTheme.typography.titleLarge
)
}
}
} ?: item { LoadingIndicator() }
} }
} }

View File

@ -13,15 +13,13 @@ import androidx.compose.foundation.lazy.LazyListScope
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.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
@ -49,7 +47,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
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.patches.OptionField 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_SUPPORTED
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
@ -72,7 +70,7 @@ fun PatchesSelectorScreen(
if (vm.compatibleVersions.isNotEmpty()) if (vm.compatibleVersions.isNotEmpty())
UnsupportedDialog( UnsupportedDialog(
appVersion = vm.selectedApp.version, appVersion = vm.input.selectedApp.version,
supportedVersions = vm.compatibleVersions, supportedVersions = vm.compatibleVersions,
onDismissRequest = vm::dismissDialogs onDismissRequest = vm::dismissDialogs
) )
@ -82,8 +80,8 @@ fun PatchesSelectorScreen(
onDismissRequest = vm::dismissDialogs, onDismissRequest = vm::dismissDialogs,
patch = patch, patch = patch,
values = vm.getOptions(bundle, patch), values = vm.getOptions(bundle, patch),
set = { key, value -> vm.setOption(bundle, patch, key, value) }, reset = { vm.resetOptions(bundle, patch) },
unset = { vm.unsetOption(bundle, patch, it) } set = { key, value -> vm.setOption(bundle, patch, key, value) }
) )
} }
@ -336,7 +334,7 @@ fun UnsupportedDialog(
fun OptionsDialog( fun OptionsDialog(
patch: PatchInfo, patch: PatchInfo,
values: Map<String, Any?>?, values: Map<String, Any?>?,
unset: (String) -> Unit, reset: () -> Unit,
set: (String, Any?) -> Unit, set: (String, Any?) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) = Dialog( ) = Dialog(
@ -350,36 +348,26 @@ fun OptionsDialog(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = patch.name, title = patch.name,
onBackClick = onDismissRequest onBackClick = onDismissRequest,
actions = {
IconButton(onClick = reset) {
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->
Column( LazyColumn(
modifier = Modifier modifier = Modifier.padding(paddingValues)
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) { ) {
patch.options?.forEach { if (patch.options == null) return@LazyColumn
ListItem(
headlineContent = { Text(it.title) },
supportingContent = { Text(it.description) },
overlineContent = {
Button(onClick = { unset(it.key) }) {
Text("reset")
}
},
trailingContent = {
val key = it.key
val value =
if (values == null || !values.contains(key)) it.defaultValue else values[key]
OptionField(option = it, value = value, setValue = { set(key, it) }) items(patch.options, key = { it.key }) { option ->
} val key = option.key
) val value =
} if (values == null || !values.contains(key)) option.defaultValue else values[key]
TextButton(onClick = onDismissRequest) { OptionItem(option = option, value = value, setValue = { set(key, it) })
Text(stringResource(R.string.apply))
} }
} }
} }

View File

@ -27,6 +27,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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.unit.dp import androidx.compose.ui.unit.dp
@ -89,7 +90,7 @@ fun VersionSelectorScreen(
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
viewModel.installedApp?.let { packageInfo -> viewModel.installedApp?.let { (packageInfo, alreadyPatched) ->
SelectedApp.Installed( SelectedApp.Installed(
packageName = viewModel.packageName, packageName = viewModel.packageName,
version = packageInfo.versionName version = packageInfo.versionName
@ -98,7 +99,8 @@ fun VersionSelectorScreen(
selectedApp = it, selectedApp = it,
selected = selectedVersion == it, selected = selectedVersion == it,
onClick = { selectedVersion = it }, onClick = { selectedVersion = it },
patchCount = supportedVersions[it.version] patchCount = supportedVersions[it.version],
alreadyPatched = alreadyPatched
) )
} }
} }
@ -132,14 +134,13 @@ fun VersionSelectorScreen(
} }
} }
const val alreadyPatched = false
@Composable @Composable
fun SelectedAppItem( fun SelectedAppItem(
selectedApp: SelectedApp, selectedApp: SelectedApp,
selected: Boolean, selected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
patchCount: Int? patchCount: Int?,
alreadyPatched: Boolean = false
) { ) {
ListItem( ListItem(
leadingContent = { RadioButton(selected, null) }, leadingContent = { RadioButton(selected, null) },
@ -161,6 +162,11 @@ fun SelectedAppItem(
trailingContent = patchCount?.let { { trailingContent = patchCount?.let { {
Text(pluralStringResource(R.plurals.patches_count, it, it)) Text(pluralStringResource(R.plurals.patches_count, it, it))
} }, } },
modifier = Modifier.clickable(onClick = onClick) modifier = Modifier
.clickable(enabled = !alreadyPatched, onClick = onClick)
.run {
if (alreadyPatched) alpha(0.5f)
else this
}
) )
} }

View File

@ -56,7 +56,7 @@ fun UpdateProgressScreen(
), style = MaterialTheme.typography.headlineMedium ), style = MaterialTheme.typography.headlineMedium
) )
LinearProgressIndicator( LinearProgressIndicator(
progress = vm.downloadProgress / 100f, progress = vm.downloadProgress,
modifier = Modifier modifier = Modifier
.padding(vertical = 16.dp) .padding(vertical = 16.dp)
.fillMaxWidth() .fillMaxWidth()
@ -66,7 +66,7 @@ fun UpdateProgressScreen(
vm.totalSize.div( vm.totalSize.div(
1000000 1000000
) )
} MB (${vm.downloadProgress.toInt()}%)" else stringResource(R.string.installing_message), } MB (${vm.downloadProgress.times(100).toInt()}%)" else stringResource(R.string.installing_message),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),

View File

@ -0,0 +1,96 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.service.UninstallService
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class AppInfoViewModel(
val installedApp: InstalledApp
) : ViewModel(), KoinComponent {
private val app: Application by inject()
private val pm: PM by inject()
private val installedAppRepository: InstalledAppRepository by inject()
lateinit var onBackClick: () -> Unit
var appInfo: PackageInfo? by mutableStateOf(null)
private set
var appliedPatches: PatchesSelection? by mutableStateOf(null)
fun launch() = pm.launch(installedApp.currentPackageName)
fun uninstall() {
when (installedApp.installType) {
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName)
InstallType.ROOT -> TODO()
}
}
private val uninstallBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
UninstallService.APP_UNINSTALL_ACTION -> {
val extraStatus = intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
val extraStatusMessage = intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
viewModelScope.launch {
installedAppRepository.delete(installedApp)
withContext(Dispatchers.Main) { onBackClick() }
}
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
app.toast(app.getString(R.string.uninstall_app_fail, extraStatusMessage))
}
}
}
}
}
init {
viewModelScope.launch {
appInfo = withContext(Dispatchers.IO) {
pm.getPackageInfo(installedApp.currentPackageName)
}
}
viewModelScope.launch {
appliedPatches = withContext(Dispatchers.IO) {
installedAppRepository.getAppliedPatches(installedApp.currentPackageName)
}
}
app.registerReceiver(
uninstallBroadcastReceiver,
IntentFilter(UninstallService.APP_UNINSTALL_ACTION)
)
}
override fun onCleared() {
super.onCleared()
app.unregisterReceiver(uninstallBroadcastReceiver)
}
}

View File

@ -3,22 +3,21 @@ package app.revanced.manager.ui.viewmodel
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.repository.ReVancedRepository import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedGitRepository
import app.revanced.manager.network.utils.getOrNull import app.revanced.manager.network.utils.getOrNull
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class ContributorViewModel(private val repository: ReVancedRepository): ViewModel() { class ContributorViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() {
val repositories = mutableStateListOf<app.revanced.manager.network.dto.ReVancedRepository>() val repositories = mutableStateListOf<ReVancedGitRepository>()
init { init {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) { reVancedAPI.getContributors().getOrNull() }?.let(
val repos = repository.getContributors().getOrNull()?.repositories repositories::addAll
withContext(Dispatchers.Main) { )
if (repos != null) { repositories.addAll(repos) }
}
}
} }
} }
} }

View File

@ -2,7 +2,7 @@ package app.revanced.manager.ui.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.DownloadedApp import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
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.util.mutableStateSetOf import app.revanced.manager.util.mutableStateSetOf

View File

@ -0,0 +1,33 @@
package app.revanced.manager.ui.viewmodel
import android.content.pm.PackageInfo
import androidx.compose.runtime.mutableStateMapOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.util.PM
import app.revanced.manager.util.collectEach
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class InstalledAppsViewModel(
private val installedAppsRepository: InstalledAppRepository,
private val pm: PM
) : ViewModel() {
val apps = installedAppsRepository.getAll().flowOn(Dispatchers.IO)
val packageInfoMap = mutableStateMapOf<String, PackageInfo?>()
init {
viewModelScope.launch {
apps.collectEach { installedApp ->
packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) {
pm.getPackageInfo(installedApp.currentPackageName)
.also { if (it == null) installedAppsRepository.delete(installedApp) }
}
}
}
}
}

View File

@ -18,8 +18,10 @@ import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.WorkerRepository import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.patcher.worker.PatcherProgressManager import app.revanced.manager.patcher.worker.PatcherProgressManager
import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.patcher.worker.PatcherWorker
@ -50,6 +52,7 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
private val app: Application by inject() private val app: Application by inject()
private val pm: PM by inject() private val pm: PM by inject()
private val workerRepository: WorkerRepository by inject() private val workerRepository: WorkerRepository by inject()
private val installedAppReceiver: InstalledAppRepository by inject()
val packageName: String = input.selectedApp.packageName val packageName: String = input.selectedApp.packageName
private val outputFile = File(app.cacheDir, "output.apk") private val outputFile = File(app.cacheDir, "output.apk")
@ -113,6 +116,15 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
app.toast(app.getString(R.string.install_app_success)) app.toast(app.getString(R.string.install_app_success))
installedPackageName = installedPackageName =
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME) intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
viewModelScope.launch {
installedAppReceiver.add(
installedPackageName!!,
packageName,
input.selectedApp.version,
InstallType.DEFAULT,
input.selectedPatches
)
}
} else { } else {
app.toast(app.getString(R.string.install_app_fail, extra)) app.toast(app.getString(R.string.install_app_fail, extra))
} }

View File

@ -17,7 +17,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.SnapshotStateSet import app.revanced.manager.util.SnapshotStateSet
@ -37,7 +37,7 @@ import org.koin.core.component.get
@Stable @Stable
@OptIn(SavedStateHandleSaveableApi::class) @OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel( class PatchesSelectorViewModel(
val selectedApp: SelectedApp val input: Destination.PatchesSelector
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent {
private val selectionRepository: PatchSelectionRepository = get() private val selectionRepository: PatchSelectionRepository = get()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
@ -54,10 +54,10 @@ class PatchesSelectorViewModel(
val unsupported = mutableListOf<PatchInfo>() val unsupported = mutableListOf<PatchInfo>()
val universal = mutableListOf<PatchInfo>() val universal = mutableListOf<PatchInfo>()
bundle.patches.filter { it.compatibleWith(selectedApp.packageName) }.forEach { bundle.patches.filter { it.compatibleWith(input.selectedApp.packageName) }.forEach {
val targetList = when { val targetList = when {
it.compatiblePackages == null -> universal it.compatiblePackages == null -> universal
it.supportsVersion(selectedApp.version) -> supported it.supportsVersion(input.selectedApp.version) -> supported
else -> unsupported else -> unsupported
} }
@ -75,7 +75,8 @@ class PatchesSelectorViewModel(
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
val bundles = bundlesFlow.first() val bundles = bundlesFlow.first()
val filteredSelection = val filteredSelection =
selectionRepository.getSelection(selectedApp.packageName) (input.patchesSelection
?: selectionRepository.getSelection(input.selectedApp.packageName))
.mapValues { (uid, patches) -> .mapValues { (uid, patches) ->
// Filter out patches that don't exist. // Filter out patches that don't exist.
val filteredPatches = bundles.singleOrNull { it.uid == uid } val filteredPatches = bundles.singleOrNull { it.uid == uid }
@ -125,7 +126,7 @@ class PatchesSelectorViewModel(
suspend fun getAndSaveSelection(): PatchesSelection = suspend fun getAndSaveSelection(): PatchesSelection =
selectedPatches.also { selectedPatches.also {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
selectionRepository.updateSelection(selectedApp.packageName, it) selectionRepository.updateSelection(input.selectedApp.packageName, it)
} }
}.mapValues { it.value.toMutableSet() }.apply { }.mapValues { it.value.toMutableSet() }.apply {
if (allowExperimental.get()) { if (allowExperimental.get()) {
@ -145,8 +146,8 @@ class PatchesSelectorViewModel(
patchOptions.getOrCreate(bundle).getOrCreate(patch.name)[key] = value patchOptions.getOrCreate(bundle).getOrCreate(patch.name)[key] = value
} }
fun unsetOption(bundle: Int, patch: PatchInfo, key: String) { fun resetOptions(bundle: Int, patch: PatchInfo) {
patchOptions[bundle]?.get(patch.name)?.remove(key) patchOptions[bundle]?.remove(patch.name)
} }
fun dismissDialogs() { fun dismissDialogs() {
@ -158,7 +159,7 @@ class PatchesSelectorViewModel(
val set = HashSet<String>() val set = HashSet<String>()
unsupportedVersions.forEach { patch -> unsupportedVersions.forEach { patch ->
patch.compatiblePackages?.find { it.packageName == selectedApp.packageName } patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName }
?.let { compatiblePackage -> ?.let { compatiblePackage ->
set.addAll(compatiblePackage.versions) set.addAll(compatiblePackage.versions)
} }

View File

@ -8,24 +8,36 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.network.api.ManagerAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.util.APK_MIMETYPE
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
import io.ktor.client.plugins.onDownload
import io.ktor.client.request.url
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
class UpdateProgressViewModel( class UpdateProgressViewModel(
app: Application, app: Application,
private val managerAPI: ManagerAPI, private val reVancedAPI: ReVancedAPI,
private val http: HttpService,
private val pm: PM private val pm: PM
) : ViewModel() { ) : ViewModel() {
var downloadedSize by mutableStateOf(0L)
private set
var totalSize by mutableStateOf(0L)
private set
val downloadProgress by derivedStateOf {
if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f
val downloadProgress by derivedStateOf { managerAPI.downloadProgress?.times(100) ?: 0f } downloadedSize.toFloat() / totalSize.toFloat()
val downloadedSize by derivedStateOf { managerAPI.downloadedSize ?: 0L } }
val totalSize by derivedStateOf { managerAPI.totalSize ?: 0L } val isInstalling by derivedStateOf { downloadProgress >= 1 }
val isInstalling by derivedStateOf { downloadProgress >= 100 }
var finished by mutableStateOf(false) var finished by mutableStateOf(false)
private set private set
@ -33,7 +45,18 @@ class UpdateProgressViewModel(
private val job = viewModelScope.launch { private val job = viewModelScope.launch {
uiSafe(app, R.string.download_manager_failed, "Failed to download manager") { uiSafe(app, R.string.download_manager_failed, "Failed to download manager") {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
managerAPI.downloadManager(location) val asset = reVancedAPI
.getRelease("revanced-manager")
.getOrThrow()
.findAssetByType(APK_MIMETYPE)
http.download(location) {
url(asset.downloadUrl)
onDownload { bytesSentTotal, contentLength ->
downloadedSize = bytesSentTotal
totalSize = contentLength
}
}
} }
finished = true finished = true
} }

View File

@ -7,7 +7,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.domain.repository.DownloadedAppRepository import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.network.downloader.APKMirror import app.revanced.manager.network.downloader.APKMirror
import app.revanced.manager.network.downloader.AppDownloader import app.revanced.manager.network.downloader.AppDownloader
@ -17,6 +19,7 @@ import app.revanced.manager.util.mutableStateSetOf
import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.simpleMessage
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -28,11 +31,12 @@ class VersionSelectorViewModel(
val packageName: String val packageName: String
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent {
private val downloadedAppRepository: DownloadedAppRepository by inject() private val downloadedAppRepository: DownloadedAppRepository by inject()
private val installedAppRepository: InstalledAppRepository by inject()
private val patchBundleRepository: PatchBundleRepository by inject() private val patchBundleRepository: PatchBundleRepository by inject()
private val pm: PM by inject() private val pm: PM by inject()
private val appDownloader: AppDownloader = APKMirror() private val appDownloader: AppDownloader = APKMirror()
var installedApp: PackageInfo? by mutableStateOf(null) var installedApp: Pair<PackageInfo, Boolean>? by mutableStateOf(null)
private set private set
var isLoading by mutableStateOf(true) var isLoading by mutableStateOf(true)
private set private set
@ -67,7 +71,17 @@ class VersionSelectorViewModel(
init { init {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
installedApp = withContext(Dispatchers.IO) { pm.getPackageInfo(packageName) } val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
val alreadyPatched = async(Dispatchers.IO) {
installedAppRepository.get(packageName)
?.let { it.installType == InstallType.DEFAULT }
?: false
}
installedApp =
packageInfo.await()?.let {
it to alreadyPatched.await()
}
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {

View File

@ -53,21 +53,18 @@ class PM(
.eachCount() .eachCount()
compatiblePackages.keys.map { pkg -> compatiblePackages.keys.map { pkg ->
try { getPackageInfo(pkg)?.let { packageInfo ->
val packageInfo = app.packageManager.getPackageInfo(pkg, 0)
AppInfo( AppInfo(
pkg, pkg,
compatiblePackages[pkg], compatiblePackages[pkg],
packageInfo, packageInfo,
File(packageInfo.applicationInfo.sourceDir) File(packageInfo.applicationInfo.sourceDir)
) )
} catch (e: NameNotFoundException) { } ?: AppInfo(
AppInfo( pkg,
pkg, compatiblePackages[pkg],
compatiblePackages[pkg], null
null )
)
}
} }
} }

View File

@ -89,4 +89,12 @@ val Color.hexCode: String
val g: Int = (green * 255).toInt() val g: Int = (green * 255).toInt()
val b: Int = (blue * 255).toInt() val b: Int = (blue * 255).toInt()
return java.lang.String.format(Locale.getDefault(), "%02X%02X%02X%02X", r, g, b, a) return java.lang.String.format(Locale.getDefault(), "%02X%02X%02X%02X", r, g, b, a)
} }
suspend fun <T> Flow<Iterable<T>>.collectEach(block: suspend (T) -> Unit) {
this.collect { iterable ->
iterable.forEach {
block(it)
}
}
}

View File

@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<plurals name="patches_count"> <plurals name="patches_count">
<item quantity="one">%d Patch</item> <item quantity="one">%d patch</item>
<item quantity="other">%d Patches</item> <item quantity="other">%d patches</item>
</plurals>
<plurals name="applied_patches">
<item quantity="one">%d applied patch</item>
<item quantity="other">%d applied patches</item>
</plurals> </plurals>
</resources> </resources>

View File

@ -13,8 +13,13 @@
<string name="import_">Import</string> <string name="import_">Import</string>
<string name="import_bundle">Import patch bundle</string> <string name="import_bundle">Import patch bundle</string>
<string name="bundle_information">Bundle information</string>
<string name="bundle_patches">Bundle patches</string> <string name="bundle_patches">Bundle patches</string>
<string name="patch_bundle_field">Patch bundle</string>
<string name="integrations_field">Integrations</string>
<string name="file_field_set">Provided</string>
<string name="file_field_not_set">Not provided</string>
<string name="field_not_set">Not set</string>
<string name="bundle_missing">Missing</string> <string name="bundle_missing">Missing</string>
<string name="bundle_error">Error</string> <string name="bundle_error">Error</string>
@ -85,8 +90,11 @@
<string name="options">Options</string> <string name="options">Options</string>
<string name="ok">OK</string> <string name="ok">OK</string>
<string name="reset">Reset</string>
<string name="patch">Patch</string> <string name="patch">Patch</string>
<string name="select_from_storage">Select from storage</string> <string name="select_from_storage">Select from storage</string>
<string name="select_from_storage_description">Select an APK file from storage using file picker</string>
<string name="type_anything">Type anything to continue</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="apply">Apply</string> <string name="apply">Apply</string>
<string name="help">Help</string> <string name="help">Help</string>
@ -149,10 +157,28 @@
<string name="loading">Loading…</string> <string name="loading">Loading…</string>
<string name="not_installed">Not installed</string> <string name="not_installed">Not installed</string>
<string name="app_info">App info</string>
<string name="uninstall">Uninstall</string>
<string name="repatch">Repatch</string>
<string name="install_type">Installation type</string>
<string name="package_name">Package name</string>
<string name="original_package_name">Original package name</string>
<string name="applied_patches">Applied patches</string>
<string name="view_applied_patches">View applied patches</string>
<string name="default_install">Default</string>
<string name="root_install">Root</string>
<string name="error_occurred">An error occurred</string> <string name="error_occurred">An error occurred</string>
<string name="already_downloaded">Already downloaded</string> <string name="already_downloaded">Already downloaded</string>
<string name="select_file">Select file</string> <string name="string_option_icon_description">Edit</string>
<string name="string_option_menu_description">More options</string>
<string name="string_option_placeholder">Value</string>
<string name="path_selector">Select from storage</string>
<string name="path_selector_parent_dir">Previous directory</string>
<string name="path_selector_dirs">Directories</string>
<string name="path_selector_files">Files</string>
<string name="show_password_field">Show password</string> <string name="show_password_field">Show password</string>
<string name="hide_password_field">Hide password</string> <string name="hide_password_field">Hide password</string>
@ -161,6 +187,7 @@
<string name="install_app">Install</string> <string name="install_app">Install</string>
<string name="install_app_success">App installed</string> <string name="install_app_success">App installed</string>
<string name="install_app_fail">Failed to install app: %s</string> <string name="install_app_fail">Failed to install app: %s</string>
<string name="uninstall_app_fail">Failed to uninstall app: %s</string>
<string name="open_app">Open</string> <string name="open_app">Open</string>
<string name="export_app">Export</string> <string name="export_app">Export</string>
<string name="export_app_success">Apk exported</string> <string name="export_app_success">Apk exported</string>

View File

@ -5,23 +5,23 @@ splash-screen = "1.0.1"
compose-activity = "1.7.2" compose-activity = "1.7.2"
paging = "3.1.1" paging = "3.1.1"
preferences-datastore = "1.0.0" preferences-datastore = "1.0.0"
work-runtime = "2.8.1ō" work-runtime = "2.8.1"
compose-bom = "2023.06.01" compose-bom = "2023.06.01"
accompanist = "0.30.1" accompanist = "0.30.1"
serialization = "1.5.1" serialization = "1.6.0"
collection = "0.3.5" collection = "0.3.5"
room-version = "2.5.2" room-version = "2.5.2"
patcher = "11.0.4" patcher = "12.1.1"
apksign = "8.0.2" apksign = "8.1.1"
bcpkix-jdk18on = "1.75" bcpkix-jdk18on = "1.76"
koin-version = "3.4.2" koin-version = "3.4.2"
koin-version-compose = "3.4.5" koin-version-compose = "3.4.5"
reimagined-navigation = "1.4.0" reimagined-navigation = "1.4.0"
ktor = "2.3.2" ktor = "2.3.2"
markdown = "0.4.1" markdown = "0.4.1"
androidGradlePlugin = "8.0.2" androidGradlePlugin = "8.1.1"
kotlinGradlePlugin = "1.8.22" kotlinGradlePlugin = "1.9.0"
devToolsGradlePlugin = "1.8.22-1.0.11" devToolsGradlePlugin = "1.9.0-1.0.12"
aboutLibrariesGradlePlugin = "10.8.2" aboutLibrariesGradlePlugin = "10.8.2"
coil = "2.4.0" coil = "2.4.0"
app-icon-loader-coil = "1.5.0" app-icon-loader-coil = "1.5.0"
@ -102,4 +102,4 @@ markdown = { group = "org.jetbrains", name = "markdown", version.ref = "markdown
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinGradlePlugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinGradlePlugin" }
devtools = { id = "com.google.devtools.ksp", version.ref = "devToolsGradlePlugin" } devtools = { id = "com.google.devtools.ksp", version.ref = "devToolsGradlePlugin" }
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibrariesGradlePlugin" } about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibrariesGradlePlugin" }

Binary file not shown.

View File

@ -1,8 +1,7 @@
#Wed Jul 12 20:30:33 ICT 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=03ec176d388f2aa99defcadc3ac6adf8dd2bce5145a129659537c0874dea5ad1 distributionSha256Sum=591855b517fc635b9e04de1d05d5e76ada3f89f5fc76f87978d1b245b4f69225
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

3
gradlew vendored
View File

@ -83,7 +83,8 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum