mirror of
https://github.com/revanced/revanced-manager
synced 2024-05-14 13:56:57 +02:00
feat: app selector screen
This commit is contained in:
parent
9065c0d260
commit
54f0a69596
@ -46,29 +46,29 @@ dependencies {
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("androidx.activity:activity-compose:1.7.1")
|
||||
implementation("androidx.paging:paging-common-ktx:3.1.1")
|
||||
|
||||
// Compose
|
||||
implementation(platform("androidx.compose:compose-bom:2023.04.01"))
|
||||
implementation(platform("androidx.compose:compose-bom:2023.05.00"))
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.paging:paging-common-ktx:3.1.1")
|
||||
implementation("androidx.core:core-ktx:1.10.0")
|
||||
implementation("androidx.compose.material:material-icons-extended")
|
||||
implementation("androidx.compose.material3:material3:1.1.0-rc01")
|
||||
|
||||
// Accompanist
|
||||
val accompanistVersion = "0.30.1"
|
||||
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
|
||||
//val accompanistVersion = "0.30.1"
|
||||
//implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
|
||||
//implementation("com.google.accompanist:accompanist-placeholder-material:$accompanistVersion")
|
||||
implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
|
||||
//implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
|
||||
//implementation("com.google.accompanist:accompanist-flowlayout:$accompanistVersion")
|
||||
//implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion")
|
||||
|
||||
// Coil (async image loading, network image)
|
||||
implementation("io.coil-kt:coil-compose:2.2.2")
|
||||
|
||||
// KotlinX
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
||||
|
||||
// Material 3
|
||||
implementation("androidx.compose.material3:material3")
|
||||
|
||||
|
||||
// ReVanced
|
||||
implementation("app.revanced:revanced-patcher:7.0.0")
|
||||
|
||||
@ -86,4 +86,5 @@ dependencies {
|
||||
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
||||
}
|
||||
|
||||
}
|
@ -38,7 +38,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service android:name=".installer.service.InstallService" />
|
||||
<service android:name=".installer.service.UninstallService" />
|
||||
<service android:name=".service.InstallService" />
|
||||
<service android:name=".service.UninstallService" />
|
||||
</application>
|
||||
</manifest>
|
@ -6,24 +6,38 @@ import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import app.revanced.manager.compose.destination.Destination
|
||||
import app.revanced.manager.compose.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.compose.ui.destination.Destination
|
||||
import app.revanced.manager.compose.ui.screen.AppSelectorScreen
|
||||
import app.revanced.manager.compose.ui.screen.DashboardScreen
|
||||
import app.revanced.manager.compose.ui.theme.ReVancedManagerTheme
|
||||
import app.revanced.manager.compose.ui.theme.Theme
|
||||
import app.revanced.manager.compose.util.PM
|
||||
import dev.olshevski.navigation.reimagined.AnimatedNavHost
|
||||
import dev.olshevski.navigation.reimagined.NavBackHandler
|
||||
import dev.olshevski.navigation.reimagined.navigate
|
||||
import dev.olshevski.navigation.reimagined.pop
|
||||
import dev.olshevski.navigation.reimagined.rememberNavController
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val prefs: PreferencesManager by inject()
|
||||
private val mainScope = MainScope()
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
installSplashScreen()
|
||||
|
||||
val context = this
|
||||
mainScope.launch(Dispatchers.IO) {
|
||||
PM.loadApps(context)
|
||||
}
|
||||
|
||||
setContent {
|
||||
ReVancedManagerTheme(
|
||||
darkTheme = prefs.theme == Theme.SYSTEM && isSystemInDarkTheme() || prefs.theme == Theme.DARK,
|
||||
@ -34,12 +48,18 @@ class MainActivity : ComponentActivity() {
|
||||
NavBackHandler(navController)
|
||||
|
||||
AnimatedNavHost(
|
||||
controller = navController,
|
||||
controller = navController
|
||||
) { destination ->
|
||||
when (destination) {
|
||||
is Destination.Dashboard -> {
|
||||
DashboardScreen()
|
||||
}
|
||||
|
||||
is Destination.Dashboard -> { DashboardScreen(
|
||||
onAppSelectorClick = { navController.navigate(Destination.AppSelector) }
|
||||
) }
|
||||
|
||||
is Destination.AppSelector -> AppSelectorScreen(
|
||||
onBackClick = { navController.pop() }
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,68 +0,0 @@
|
||||
package app.revanced.manager.compose.installer.utils
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import app.revanced.manager.compose.installer.service.InstallService
|
||||
import app.revanced.manager.compose.installer.service.UninstallService
|
||||
import java.io.File
|
||||
|
||||
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
||||
|
||||
object PM {
|
||||
|
||||
fun installApp(apk: File, context: Context) {
|
||||
val packageInstaller = context.packageManager.packageInstaller
|
||||
val session =
|
||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
|
||||
session.writeApk(apk)
|
||||
session.commit(context.installIntentSender)
|
||||
session.close()
|
||||
}
|
||||
|
||||
fun uninstallPackage(pkg: String, context: Context) {
|
||||
val packageInstaller = context.packageManager.packageInstaller
|
||||
packageInstaller.uninstall(pkg, context.uninstallIntentSender)
|
||||
}
|
||||
}
|
||||
|
||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||
apk.inputStream().use { inputStream ->
|
||||
openWrite(apk.name, 0, apk.length()).use { outputStream ->
|
||||
inputStream.copyTo(outputStream, byteArraySize)
|
||||
fsync(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val intentFlags
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
else
|
||||
0
|
||||
|
||||
private val sessionParams
|
||||
get() = PackageInstaller.SessionParams(
|
||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||
).apply {
|
||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
||||
}
|
||||
|
||||
private val Context.installIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, InstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
||||
|
||||
private val Context.uninstallIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, UninstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.manager.compose.installer.service
|
||||
package app.revanced.manager.compose.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.manager.compose.installer.service
|
||||
package app.revanced.manager.compose.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
@ -0,0 +1,41 @@
|
||||
package app.revanced.manager.compose.ui.component
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Android
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
|
||||
@Composable
|
||||
fun AppIcon(
|
||||
drawable: Drawable?,
|
||||
contentDescription: String?,
|
||||
size: Int = 48
|
||||
) {
|
||||
if (drawable == null) {
|
||||
val image = rememberVectorPainter(Icons.Default.Android)
|
||||
val colorFilter = ColorFilter.tint(LocalContentColor.current)
|
||||
|
||||
Image(
|
||||
image,
|
||||
contentDescription,
|
||||
Modifier.size(size.dp),
|
||||
colorFilter = colorFilter
|
||||
)
|
||||
} else {
|
||||
val image = rememberAsyncImagePainter(drawable)
|
||||
|
||||
Image(
|
||||
image,
|
||||
contentDescription,
|
||||
Modifier.size(size.dp)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package app.revanced.manager.compose.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppScaffold(
|
||||
topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {},
|
||||
bottomBar: @Composable () -> Unit = {},
|
||||
floatingActionButton: @Composable () -> Unit = {},
|
||||
content: @Composable (PaddingValues) -> Unit
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = { topBar(scrollBehavior) },
|
||||
bottomBar = bottomBar,
|
||||
floatingActionButton = floatingActionButton,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppTopBar(
|
||||
title: String,
|
||||
onBackClick: (() -> Unit)? = null,
|
||||
actions: @Composable (RowScope.() -> Unit) = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||
|
||||
TopAppBar(
|
||||
title = { Text(title) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = {
|
||||
if (onBackClick != null) {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = actions,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = containerColor
|
||||
)
|
||||
)
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package app.revanced.manager.compose.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.compose.R
|
||||
|
||||
@Composable
|
||||
fun LoadingIndicator(progress: Float? = null, text: Int? = R.string.loading_body) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (text != null)
|
||||
Text(stringResource(text))
|
||||
if (progress == null) {
|
||||
CircularProgressIndicator(modifier = Modifier.padding(vertical = 16.dp))
|
||||
} else {
|
||||
CircularProgressIndicator(progress = progress, modifier = Modifier.padding(vertical = 16.dp))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.manager.compose.destination
|
||||
package app.revanced.manager.compose.ui.destination
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@ -8,4 +8,7 @@ sealed interface Destination: Parcelable {
|
||||
@Parcelize
|
||||
object Dashboard: Destination
|
||||
|
||||
@Parcelize
|
||||
object AppSelector: Destination
|
||||
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
package app.revanced.manager.compose.ui.screen
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material3.*
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.compose.R
|
||||
import app.revanced.manager.compose.ui.component.AppIcon
|
||||
import app.revanced.manager.compose.ui.component.AppScaffold
|
||||
import app.revanced.manager.compose.ui.component.AppTopBar
|
||||
import app.revanced.manager.compose.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.compose.util.PM
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppSelectorScreen(
|
||||
onBackClick: () -> Unit
|
||||
) {
|
||||
var filterText by rememberSaveable { mutableStateOf("") }
|
||||
var search by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
// TODO: find something better for this
|
||||
if (search) {
|
||||
SearchBar(
|
||||
query = filterText,
|
||||
onQueryChange = { filterText = it },
|
||||
onSearch = { },
|
||||
active = true,
|
||||
onActiveChange = { search = it },
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
placeholder = { Text(stringResource(R.string.search_apps)) },
|
||||
leadingIcon = { IconButton({ search = false }) { Icon(Icons.Default.ArrowBack, null) } },
|
||||
shape = SearchBarDefaults.inputFieldShape,
|
||||
content = {
|
||||
if (PM.appList.isNotEmpty()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(
|
||||
PM.appList
|
||||
.filter { app ->
|
||||
(app.label.contains(
|
||||
filterText,
|
||||
true
|
||||
) or app.packageName.contains(filterText, true))
|
||||
}
|
||||
) { app ->
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { },
|
||||
leadingContent = { AppIcon(app.icon, null, 36) },
|
||||
headlineContent = { Text(app.label) },
|
||||
supportingContent = { Text(app.packageName) },
|
||||
trailingContent = { Text((PM.testList[app.packageName]?: 0).let { if (it == 1) "$it Patch" else "$it Patches" }) }
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LoadingIndicator()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = "Select an app",
|
||||
onBackClick = onBackClick,
|
||||
actions = {
|
||||
IconButton({}) {
|
||||
Icon(Icons.Outlined.HelpOutline, "Help")
|
||||
}
|
||||
IconButton(onClick = { search = true }) {
|
||||
Icon(Icons.Outlined.Search, "Search")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
if (PM.supportedAppList.isNotEmpty()) {
|
||||
|
||||
/*Row(
|
||||
modifier = Modifier.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
FilterChip(
|
||||
selected = false,
|
||||
onClick = {},
|
||||
label = { Text("Patched apps") },
|
||||
leadingIcon = { Icon(Icons.Default.Check, null) },
|
||||
enabled = false
|
||||
)
|
||||
FilterChip(
|
||||
selected = false,
|
||||
onClick = {},
|
||||
label = { Text("User apps") },
|
||||
leadingIcon = { Icon(Icons.Default.Android, null) }
|
||||
)
|
||||
FilterChip(
|
||||
selected = filterSystemApps,
|
||||
onClick = { filterSystemApps = !filterSystemApps },
|
||||
label = { Text("System apps") },
|
||||
leadingIcon = { Icon(Icons.Default.Apps, null) }
|
||||
)
|
||||
}*/
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { },
|
||||
leadingContent = { Box(Modifier.size(36.dp), Alignment.Center) { Icon(Icons.Default.Storage, null, modifier = Modifier.size(24.dp)) } },
|
||||
headlineContent = { Text("Select from storage") }
|
||||
)
|
||||
|
||||
Divider()
|
||||
|
||||
}
|
||||
|
||||
(PM.appList.ifEmpty { PM.supportedAppList }).also { list ->
|
||||
items(
|
||||
count = list.size,
|
||||
key = { list[it].packageName }
|
||||
) { index ->
|
||||
val app = list[index]
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { },
|
||||
leadingContent = { AppIcon(app.icon, null, 36) },
|
||||
headlineContent = { Text(app.label) },
|
||||
supportingContent = { Text(app.packageName) },
|
||||
trailingContent = {
|
||||
Text(
|
||||
(PM.testList[app.packageName]?: 0).let { if (it == 1) "$it Patch" else "$it Patches" }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
if (PM.appList.isEmpty()) {
|
||||
item {
|
||||
Box(Modifier.fillMaxWidth(), Alignment.Center) {
|
||||
CircularProgressIndicator(Modifier.padding(vertical = 15.dp).size(24.dp), strokeWidth = 3.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LoadingIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,16 +15,18 @@ 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.TopAppBar
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.compose.R
|
||||
import app.revanced.manager.compose.ui.component.AppScaffold
|
||||
import app.revanced.manager.compose.ui.component.AppTopBar
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
enum class DashboardPage(
|
||||
@ -37,16 +39,18 @@ enum class DashboardPage(
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DashboardScreen() {
|
||||
fun DashboardScreen(
|
||||
onAppSelectorClick: () -> Unit
|
||||
) {
|
||||
val pages: Array<DashboardPage> = DashboardPage.values()
|
||||
|
||||
val pagerState = rememberPagerState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("ReVanced Manager") },
|
||||
AppTopBar(
|
||||
title = "ReVanced Manager",
|
||||
actions = {
|
||||
IconButton(onClick = {}) {
|
||||
Icon(imageVector = Icons.Outlined.Info, contentDescription = null)
|
||||
@ -61,13 +65,20 @@ fun DashboardScreen() {
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = {}) {
|
||||
FloatingActionButton(onClick = {
|
||||
if (pagerState.currentPage == DashboardPage.DASHBOARD.ordinal)
|
||||
onAppSelectorClick()
|
||||
}
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Add, contentDescription = null)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(Modifier.padding(paddingValues)) {
|
||||
TabRow(selectedTabIndex = pagerState.currentPage) {
|
||||
TabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||
) {
|
||||
pages.forEachIndexed { index, page ->
|
||||
val title = stringResource(id = page.titleResId)
|
||||
Tab(
|
||||
|
@ -2,22 +2,20 @@ package app.revanced.manager.compose.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.revanced.manager.compose.R
|
||||
|
||||
@Composable
|
||||
fun InstalledAppsScreen() {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_patched_apps_found),
|
||||
fontSize = 24.sp,
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.Center)
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
}
|
@ -2,22 +2,20 @@ package app.revanced.manager.compose.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.revanced.manager.compose.R
|
||||
|
||||
@Composable
|
||||
fun SourcesScreen() {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_sources_set),
|
||||
fontSize = 24.sp,
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.Center)
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
}
|
@ -2,14 +2,14 @@ package app.revanced.manager.compose.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
@ -25,8 +25,8 @@ private val LightColorScheme = lightColorScheme(
|
||||
|
||||
@Composable
|
||||
fun ReVancedManagerTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
darkTheme: Boolean,
|
||||
dynamicColor: Boolean,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
@ -37,11 +37,19 @@ fun ReVancedManagerTheme(
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
(view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
|
||||
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
|
||||
val activity = view.context as Activity
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(activity.window, false)
|
||||
|
||||
activity.window.navigationBarColor = colorScheme.background.toArgb()
|
||||
activity.window.statusBarColor = Color.Transparent.toArgb()
|
||||
|
||||
WindowCompat.getInsetsController(activity.window, view).isAppearanceLightStatusBars = !darkTheme
|
||||
WindowCompat.getInsetsController(activity.window, view).isAppearanceLightNavigationBars = !darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
|
161
app/src/main/java/app/revanced/manager/compose/util/PM.kt
Normal file
161
app/src/main/java/app/revanced/manager/compose/util/PM.kt
Normal file
@ -0,0 +1,161 @@
|
||||
package app.revanced.manager.compose.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import app.revanced.manager.compose.service.InstallService
|
||||
import app.revanced.manager.compose.service.UninstallService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.RawValue
|
||||
import java.io.File
|
||||
|
||||
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
||||
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
@Suppress("DEPRECATION")
|
||||
object PM {
|
||||
|
||||
val testList = mapOf(
|
||||
"com.google.android.youtube" to 59,
|
||||
"com.android.vending" to 34,
|
||||
"com.backdrops.wallpapers" to 2,
|
||||
"com.termux" to 2,
|
||||
"com.notinstalled.app" to 1,
|
||||
"com.2notinstalled.app" to 1,
|
||||
"org.adaway" to 5,
|
||||
"com.activitymanager" to 1,
|
||||
"com.guoshi.httpcanary" to 1,
|
||||
"org.lsposed.lspatch" to 1,
|
||||
"app.revanced.manager.flutter" to 100,
|
||||
"com.reddit.frontpage" to 20
|
||||
)
|
||||
|
||||
val appList = mutableStateListOf<AppInfo>()
|
||||
val supportedAppList = mutableStateListOf<AppInfo>()
|
||||
|
||||
suspend fun loadApps(context: Context) {
|
||||
val packageManager = context.packageManager
|
||||
|
||||
testList.keys.map {
|
||||
try {
|
||||
val applicationInfo = packageManager.getApplicationInfo(it, 0)
|
||||
|
||||
AppInfo(
|
||||
it,
|
||||
applicationInfo.loadLabel(packageManager).toString(),
|
||||
applicationInfo.loadIcon(packageManager),
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
AppInfo(
|
||||
it,
|
||||
"Not installed"
|
||||
)
|
||||
}
|
||||
}.let { list ->
|
||||
list.sortedWith(
|
||||
compareByDescending<AppInfo> {
|
||||
testList[it.packageName]
|
||||
}.thenBy { it.label }.thenBy { it.packageName }
|
||||
)
|
||||
}.also {
|
||||
withContext(Dispatchers.Main) { supportedAppList.addAll(it) }
|
||||
}
|
||||
|
||||
val localAppList = mutableListOf<AppInfo>()
|
||||
|
||||
packageManager.getInstalledApplications(PackageManager.GET_META_DATA).map {
|
||||
AppInfo(
|
||||
it.packageName,
|
||||
it.loadLabel(packageManager).toString(),
|
||||
it.loadIcon(packageManager)
|
||||
)
|
||||
}.also { localAppList.addAll(it) }
|
||||
|
||||
testList.keys.mapNotNull { packageName ->
|
||||
if (!localAppList.any { packageName == it.packageName }) {
|
||||
AppInfo(
|
||||
packageName,
|
||||
"Not installed"
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.also { localAppList.addAll(it) }
|
||||
|
||||
localAppList.sortWith(
|
||||
compareByDescending<AppInfo> {
|
||||
testList[it.packageName]
|
||||
}.thenBy { it.label }.thenBy { it.packageName }
|
||||
).also {
|
||||
withContext(Dispatchers.Main) { appList.addAll(localAppList) }
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class AppInfo(
|
||||
val packageName: String,
|
||||
val label: String,
|
||||
val icon: @RawValue Drawable? = null
|
||||
) : Parcelable
|
||||
|
||||
fun installApp(apk: File, context: Context) {
|
||||
val packageInstaller = context.packageManager.packageInstaller
|
||||
val session =
|
||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
|
||||
session.writeApk(apk)
|
||||
session.commit(context.installIntentSender)
|
||||
session.close()
|
||||
}
|
||||
|
||||
fun uninstallPackage(pkg: String, context: Context) {
|
||||
val packageInstaller = context.packageManager.packageInstaller
|
||||
packageInstaller.uninstall(pkg, context.uninstallIntentSender)
|
||||
}
|
||||
}
|
||||
|
||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||
apk.inputStream().use { inputStream ->
|
||||
openWrite(apk.name, 0, apk.length()).use { outputStream ->
|
||||
inputStream.copyTo(outputStream, byteArraySize)
|
||||
fsync(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val intentFlags
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
else
|
||||
0
|
||||
|
||||
private val sessionParams
|
||||
get() = PackageInstaller.SessionParams(
|
||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||
).apply {
|
||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
||||
}
|
||||
|
||||
private val Context.installIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, InstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
||||
|
||||
private val Context.uninstallIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, UninstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
@ -1,6 +1,26 @@
|
||||
<resources>
|
||||
<string name="app_name">ReVanced Manager</string>
|
||||
<string name="patcher">Patcher</string>
|
||||
<string name="patches">Patches</string>
|
||||
<string name="integrations">Integrations</string>
|
||||
<string name="cli">CLI</string>
|
||||
<string name="manager">Manager</string>
|
||||
|
||||
<string name="dashboard">Dashboard</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="about">About</string>
|
||||
|
||||
<string name="search_apps">Search apps…</string>
|
||||
<string name="loading_body">Loading…</string>
|
||||
|
||||
<string name="downloading_patches">Downloading patch bundle…</string>
|
||||
<string name="downloading_integrations">Downloading Integrations…</string>
|
||||
|
||||
<string name="contributors">Contributors</string>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="dynamic_color">Dynamic color</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="storage">Storage</string>
|
||||
<string name="tab_apps">Apps</string>
|
||||
<string name="tab_sources">Sources</string>
|
||||
<string name="no_sources_set">No sources set</string>
|
||||
|
Loading…
Reference in New Issue
Block a user