mirror of
https://github.com/revanced/revanced-manager-compose
synced 2024-06-03 03:46:21 +02:00
Compare commits
14 Commits
e353c01258
...
f0ab415a6f
Author | SHA1 | Date | |
---|---|---|---|
|
f0ab415a6f | ||
|
ecae587491 | ||
|
4edec8afb4 | ||
|
0e6b4ed350 | ||
|
12dc8a2585 | ||
|
fe95afca6c | ||
|
0ba1ac6880 | ||
|
e37cbf19f2 | ||
|
cd75ac90d9 | ||
|
0520ff23c7 | ||
|
0dc3bed349 | ||
|
1630668360 | ||
|
d666ece4a8 | ||
|
4eca2a3b5d |
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
@ -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}}
|
||||||
|
|
|
@ -67,7 +67,7 @@ android {
|
||||||
|
|
||||||
buildFeatures.compose = true
|
buildFeatures.compose = true
|
||||||
|
|
||||||
composeOptions.kotlinCompilerExtensionVersion = "1.4.8"
|
composeOptions.kotlinCompilerExtensionVersion = "1.5.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
|
|
@ -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')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
@ -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>>
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -18,4 +18,6 @@ val viewModelModule = module {
|
||||||
viewModelOf(::ImportExportViewModel)
|
viewModelOf(::ImportExportViewModel)
|
||||||
viewModelOf(::ContributorViewModel)
|
viewModelOf(::ContributorViewModel)
|
||||||
viewModelOf(::DownloadsViewModel)
|
viewModelOf(::DownloadsViewModel)
|
||||||
|
viewModelOf(::InstalledAppsViewModel)
|
||||||
|
viewModelOf(::AppInfoViewModel)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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()
|
|
|
@ -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")
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -30,7 +30,7 @@ object Aligning {
|
||||||
}
|
}
|
||||||
|
|
||||||
file.copyEntriesFromFileAligned(
|
file.copyEntriesFromFileAligned(
|
||||||
ZipFile(inputFile),
|
ZipFile(inputFile, readonly = true),
|
||||||
ZipAligner::getEntryAlignment
|
ZipAligner::getEntryAlignment
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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) }
|
||||||
|
)
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
|
@ -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)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
3
gradlew
vendored
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user