LSPatch Manager (Coding)
This commit is contained in:
parent
9129b42226
commit
6285ebe6c4
|
|
@ -24,8 +24,8 @@ import android.util.Log;
|
|||
import com.google.gson.Gson;
|
||||
|
||||
import org.lsposed.lspatch.loader.util.FileUtils;
|
||||
import org.lsposed.lspatch.loader.util.ModuleLoader;
|
||||
import org.lsposed.lspatch.loader.util.XLog;
|
||||
import org.lsposed.lspatch.manager.ModuleLoader;
|
||||
import org.lsposed.lspatch.share.Constants;
|
||||
import org.lsposed.lspatch.share.PatchConfig;
|
||||
import org.lsposed.lspd.config.ApplicationServiceClient;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package org.lsposed.lspatch.loader.util;
|
||||
package org.lsposed.lspatch.manager;
|
||||
|
||||
import android.os.SharedMemory;
|
||||
import android.system.ErrnoException;
|
||||
|
|
@ -16,6 +16,7 @@ import java.util.List;
|
|||
import java.util.zip.ZipFile;
|
||||
|
||||
public class ModuleLoader {
|
||||
|
||||
private static final String TAG = "LSPatch";
|
||||
|
||||
private static void readDexes(ZipFile apkFile, List<SharedMemory> preLoadedDexes) {
|
||||
|
|
@ -9,6 +9,8 @@ val verName: String by rootProject.extra
|
|||
val androidSourceCompatibility: JavaVersion by rootProject.extra
|
||||
val androidTargetCompatibility: JavaVersion by rootProject.extra
|
||||
|
||||
val composeVersion = "1.2.0-alpha03"
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
|
|
@ -42,16 +44,31 @@ android {
|
|||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = composeVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":patch"))
|
||||
compileOnly(project(":patch"))
|
||||
implementation(project(":imanager"))
|
||||
|
||||
implementation("androidx.core:core-ktx:1.7.0")
|
||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||
implementation("androidx.activity:activity-compose:1.5.0-alpha01")
|
||||
implementation("androidx.compose.material:material-icons-extended:1.0.5")
|
||||
implementation("androidx.compose.material3:material3:1.0.0-alpha04")
|
||||
implementation("androidx.compose.runtime:runtime-livedata:$composeVersion")
|
||||
implementation("androidx.compose.ui:ui:$composeVersion")
|
||||
implementation("androidx.compose.ui:ui-tooling:$composeVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha01")
|
||||
implementation("androidx.navigation:navigation-compose:2.5.0-alpha01")
|
||||
implementation("androidx.preference:preference:1.2.0")
|
||||
implementation("com.google.accompanist:accompanist-drawablepainter:0.24.2-alpha")
|
||||
implementation("com.google.accompanist:accompanist-navigation-animation:0.24.2-alpha")
|
||||
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
|
||||
implementation("com.google.android.material:material:1.5.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||
//implementation("dev.rikka.rikkax.material:material:2.1.0")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@
|
|||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.LSPatch">
|
||||
android:theme="@style/Theme.Material3.DayNight.NoActionBar">
|
||||
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:name=".ui.activity.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
|
@ -26,10 +26,10 @@
|
|||
</activity>
|
||||
|
||||
<provider
|
||||
android:name=".ModuleProvider"
|
||||
android:name=".manager.ModuleProvider"
|
||||
android:authorities="org.lsposed.lspatch.provider"
|
||||
android:enabled="true"
|
||||
android:exported="true" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
package org.lsposed.lspatch
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
|
||||
const val TAG = "LSPatch Manager"
|
||||
|
||||
class LSPApplication : Application() {
|
||||
companion object {
|
||||
const val TAG = "LSPatch Manager"
|
||||
lateinit var appContext: Context
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
appContext = applicationContext
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package org.lsposed.lspatch
|
||||
package org.lsposed.lspatch.manager
|
||||
|
||||
import org.lsposed.lspatch.manager.IManagerService
|
||||
import org.lsposed.lspd.models.Module
|
||||
|
||||
class ManagerServiceImpl : IManagerService.Stub() {
|
||||
override fun getModules(): List<Module> {
|
||||
return ModuleProvider.allModules
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package org.lsposed.lspatch
|
||||
package org.lsposed.lspatch.manager
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
|
|
@ -8,11 +8,11 @@ import android.net.Uri
|
|||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import org.lsposed.lspatch.LSPApplication.Companion.TAG
|
||||
import org.lsposed.lspatch.TAG
|
||||
import org.lsposed.lspd.models.Module
|
||||
import org.lsposed.lspd.service.ConfigFileManager.loadModule
|
||||
|
||||
class ModuleProvider : ContentProvider() {
|
||||
|
||||
companion object {
|
||||
lateinit var allModules: List<Module>
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ class ModuleProvider : ContentProvider() {
|
|||
Module().apply {
|
||||
apkPath = app.publicSourceDir
|
||||
packageName = app.packageName
|
||||
file = loadModule(apkPath)
|
||||
file = ModuleLoader.loadModule(apkPath)
|
||||
}.also { list.add(it) }
|
||||
Log.d(TAG, "send module ${app.packageName}")
|
||||
}
|
||||
|
|
@ -70,4 +70,4 @@ class ModuleProvider : ContentProvider() {
|
|||
override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package org.lsposed.lspatch.ui
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import org.lsposed.lspatch.databinding.ActivityMainBinding
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
lateinit var binding: ActivityMainBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package org.lsposed.lspatch.ui.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
||||
import org.lsposed.lspatch.ui.page.PageList
|
||||
import org.lsposed.lspatch.ui.theme.LSPTheme
|
||||
import org.lsposed.lspatch.ui.util.LocalNavController
|
||||
import org.lsposed.lspatch.ui.util.currentRoute
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
val navController = rememberAnimatedNavController()
|
||||
val currentRoute = navController.currentRoute
|
||||
val currentPage = if (currentRoute == null) null else PageList.valueOf(currentRoute.substringBefore('/'))
|
||||
var mainPage by rememberSaveable { mutableStateOf(PageList.Home) }
|
||||
|
||||
LSPTheme {
|
||||
CompositionLocalProvider(LocalNavController provides navController) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
navController.currentBackStackEntry?.let {
|
||||
CompositionLocalProvider(LocalViewModelStoreOwner provides it) {
|
||||
currentPage?.topBar?.invoke()
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
MainNavigationBar(mainPage) {
|
||||
mainPage = it
|
||||
navController.navigate(it.name)
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
navController.currentBackStackEntry?.let {
|
||||
CompositionLocalProvider(LocalViewModelStoreOwner provides it) {
|
||||
currentPage?.fab?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
MainNavHost(navController, Modifier.padding(innerPadding))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainNavHost(navController: NavHostController, modifier: Modifier) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = PageList.Home.name,
|
||||
modifier = modifier
|
||||
) {
|
||||
PageList.values().forEach { page ->
|
||||
val sb = StringBuilder(page.name)
|
||||
page.arguments.forEach { sb.append("/{${it.name}}") }
|
||||
composable(route = sb.toString(), arguments = page.arguments, content = page.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainNavigationBar(page: PageList, onClick: (PageList) -> Unit) {
|
||||
NavigationBar(tonalElevation = 8.dp) {
|
||||
arrayOf(PageList.Home, PageList.Patches, PageList.Repo, PageList.Settings).forEach {
|
||||
NavigationBarItem(
|
||||
selected = page == it,
|
||||
onClick = { onClick(it) },
|
||||
icon = {
|
||||
if (page == it) Icon(it.iconSelected!!, it.title)
|
||||
else Icon(it.iconNotSelected!!, it.title)
|
||||
},
|
||||
label = { Text(it.title) },
|
||||
alwaysShowLabel = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package org.lsposed.lspatch.ui.component
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import org.lsposed.lspatch.ui.theme.LSPTheme
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun AppItem(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: Drawable,
|
||||
label: String,
|
||||
packageName: String,
|
||||
additionalInfo: (@Composable () -> Unit)? = null,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (() -> Unit)? = null
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
.padding(20.dp)
|
||||
) {
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
painter = rememberDrawablePainter(icon),
|
||||
contentDescription = label,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
Column(Modifier.padding(start = 20.dp)) {
|
||||
Text(text = label, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(text = packageName, style = MaterialTheme.typography.bodySmall)
|
||||
additionalInfo?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AppItemPreview() {
|
||||
LSPTheme {
|
||||
val shape = GradientDrawable()
|
||||
shape.shape = GradientDrawable.RECTANGLE
|
||||
shape.setColor(MaterialTheme.colorScheme.primary.toArgb())
|
||||
AppItem(
|
||||
icon = shape,
|
||||
label = "Sample App",
|
||||
packageName = "org.lsposed.sample",
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package org.lsposed.lspatch.ui.component
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object SelectionColumnScope {
|
||||
|
||||
@Composable
|
||||
fun SelectionItem(
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
desc: String? = null,
|
||||
extraContent: (@Composable ColumnScope.() -> Unit)? = null
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 64.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(
|
||||
animateColorAsState(
|
||||
if (selected) MaterialTheme.colorScheme.primaryContainer
|
||||
else MaterialTheme.colorScheme.inverseOnSurface
|
||||
).value
|
||||
)
|
||||
.clickable { onClick() }
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
if (desc != null || extraContent != null) {
|
||||
AnimatedVisibility(
|
||||
visible = selected,
|
||||
enter = fadeIn() + expandVertically(expandFrom = Alignment.Top),
|
||||
exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Bottom)
|
||||
) {
|
||||
Column {
|
||||
desc?.let {
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
extraContent?.invoke(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun SelectionColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable() (SelectionColumnScope.() -> Unit)
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.clip(RoundedCornerShape(32.dp)),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
content = { SelectionColumnScope.content() }
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package org.lsposed.lspatch.ui.page
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun HomePage() {
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeTopBar() {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package org.lsposed.lspatch.ui.page
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Api
|
||||
import androidx.compose.material.icons.outlined.AutoFixHigh
|
||||
import androidx.compose.material.icons.outlined.WorkOutline
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.lsposed.lspatch.R
|
||||
import org.lsposed.lspatch.TAG
|
||||
import org.lsposed.lspatch.ui.component.SelectionColumn
|
||||
import org.lsposed.lspatch.ui.util.LocalNavController
|
||||
import org.lsposed.lspatch.ui.viewmodel.AppInfo
|
||||
|
||||
@Composable
|
||||
fun NewPatchFab() {
|
||||
val navController = LocalNavController.current
|
||||
val patchApp by navController.currentBackStackEntry!!.savedStateHandle
|
||||
.getLiveData<AppInfo>("appInfo").observeAsState()
|
||||
if (patchApp != null) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.patch_start)) },
|
||||
icon = { Icon(Icons.Outlined.AutoFixHigh, null) },
|
||||
onClick = { /*TODO*/ }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewPatchPage() {
|
||||
val navController = LocalNavController.current
|
||||
val patchApp by navController.currentBackStackEntry!!.savedStateHandle
|
||||
.getLiveData<AppInfo>("appInfo").observeAsState()
|
||||
|
||||
Log.d(TAG, "patchApp is $patchApp")
|
||||
if (patchApp == null) {
|
||||
navController.navigate(PageList.SelectApps.name + "/false")
|
||||
} else {
|
||||
var useManager by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(top = 24.dp)
|
||||
) {
|
||||
Text(text = patchApp!!.label, style = MaterialTheme.typography.headlineSmall)
|
||||
Text(text = patchApp!!.app.packageName, style = MaterialTheme.typography.bodyLarge)
|
||||
Text(
|
||||
text = stringResource(R.string.patch_mode),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(top = 24.dp, bottom = 12.dp)
|
||||
)
|
||||
SelectionColumn {
|
||||
SelectionItem(
|
||||
selected = useManager,
|
||||
onClick = { useManager = true },
|
||||
icon = Icons.Outlined.Api,
|
||||
title = stringResource(R.string.patch_local),
|
||||
desc = stringResource(R.string.patch_local_desc)
|
||||
)
|
||||
SelectionItem(
|
||||
selected = !useManager,
|
||||
onClick = { useManager = false },
|
||||
icon = Icons.Outlined.WorkOutline,
|
||||
title = stringResource(R.string.patch_portable),
|
||||
desc = stringResource(R.string.patch_portable_desc),
|
||||
extraContent = {
|
||||
TextButton(
|
||||
onClick = { /* TODO */ }
|
||||
) {
|
||||
Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package org.lsposed.lspatch.ui.page
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.GetApp
|
||||
import androidx.compose.material.icons.filled.Healing
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.outlined.GetApp
|
||||
import androidx.compose.material.icons.outlined.Healing
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NamedNavArgument
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import org.lsposed.lspatch.R
|
||||
|
||||
enum class PageList(
|
||||
val iconSelected: ImageVector? = null,
|
||||
val iconNotSelected: ImageVector? = null,
|
||||
val arguments: List<NamedNavArgument> = emptyList(),
|
||||
val topBar: @Composable () -> Unit,
|
||||
val fab: @Composable () -> Unit = {},
|
||||
val body: @Composable NavBackStackEntry.() -> Unit
|
||||
) {
|
||||
Home(
|
||||
iconSelected = Icons.Filled.Home,
|
||||
iconNotSelected = Icons.Outlined.Home,
|
||||
topBar = { HomeTopBar() },
|
||||
body = { HomePage() }
|
||||
),
|
||||
Patches(
|
||||
iconSelected = Icons.Filled.Healing,
|
||||
iconNotSelected = Icons.Outlined.Healing,
|
||||
topBar = { PatchesTopBar() },
|
||||
fab = { PatchesFab() },
|
||||
body = {}
|
||||
),
|
||||
Repo(
|
||||
iconSelected = Icons.Filled.GetApp,
|
||||
iconNotSelected = Icons.Outlined.GetApp,
|
||||
topBar = {},
|
||||
body = {}
|
||||
),
|
||||
Settings(
|
||||
iconSelected = Icons.Filled.Settings,
|
||||
iconNotSelected = Icons.Outlined.Settings,
|
||||
topBar = {},
|
||||
body = {}
|
||||
),
|
||||
NewPatch(
|
||||
topBar = {},
|
||||
fab = { NewPatchFab() },
|
||||
body = { NewPatchPage() }
|
||||
),
|
||||
SelectApps(
|
||||
arguments = listOf(
|
||||
navArgument("multiSelect") { type = NavType.BoolType }
|
||||
),
|
||||
topBar = {},
|
||||
body = { SelectAppsPage(this) }
|
||||
);
|
||||
|
||||
val title: String
|
||||
@Composable get() = when (this) {
|
||||
Home -> stringResource(R.string.app_name)
|
||||
Patches -> stringResource(R.string.page_patches)
|
||||
Repo -> stringResource(R.string.page_repo)
|
||||
Settings -> stringResource(R.string.page_settings)
|
||||
NewPatch -> stringResource(R.string.page_new_patch)
|
||||
SelectApps -> stringResource(R.string.page_select_app)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package org.lsposed.lspatch.ui.page
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallTopAppBar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import org.lsposed.lspatch.R
|
||||
import org.lsposed.lspatch.ui.util.LocalNavController
|
||||
|
||||
@Composable
|
||||
fun PatchesTopBar() {
|
||||
SmallTopAppBar(
|
||||
title = { Text(PageList.Patches.title) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PatchesFab() {
|
||||
val navController = LocalNavController.current
|
||||
FloatingActionButton(onClick = { navController.navigate(PageList.NewPatch.name) }) {
|
||||
Icon(Icons.Filled.Add, stringResource(R.string.patches_add))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PatchesPage() {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package org.lsposed.lspatch.ui.page
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import org.lsposed.lspatch.ui.component.AppItem
|
||||
import org.lsposed.lspatch.ui.util.LocalNavController
|
||||
import org.lsposed.lspatch.ui.viewmodel.AppInfo
|
||||
import org.lsposed.lspatch.ui.viewmodel.SelectAppViewModel
|
||||
|
||||
@Composable
|
||||
fun SelectAppsPage(entry: NavBackStackEntry) {
|
||||
val multiSelect = entry.arguments?.get("multiSelect") as? Boolean
|
||||
?: throw IllegalArgumentException("multiSelect is null")
|
||||
if (multiSelect) {
|
||||
TODO("MultiSelect")
|
||||
} else {
|
||||
SelectSingle()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectSingle(
|
||||
filter: (AppInfo) -> Boolean = { true }
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val viewModel = viewModel<SelectAppViewModel>()
|
||||
val isRefreshing by viewModel.isRefreshing.collectAsState()
|
||||
if (SelectAppViewModel.appList.isEmpty())
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.loadAppList(context)
|
||||
}
|
||||
|
||||
SwipeRefresh(
|
||||
state = rememberSwipeRefreshState(isRefreshing),
|
||||
onRefresh = { viewModel.loadAppList(context) },
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
LazyColumn {
|
||||
items(SelectAppViewModel.appList) {
|
||||
AppItem(
|
||||
icon = it.icon,
|
||||
label = it.label,
|
||||
packageName = it.app.packageName,
|
||||
onClick = {
|
||||
navController.previousBackStackEntry!!.savedStateHandle.getLiveData<AppInfo>("appInfo").value = it
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package org.lsposed.lspatch.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.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.ViewCompat
|
||||
|
||||
@Composable
|
||||
fun LSPTheme(
|
||||
isDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||
enableDynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
enableDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (isDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
isDarkTheme -> darkColorScheme()
|
||||
else -> lightColorScheme()
|
||||
}
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
(view.context as Activity).window.statusBarColor = colorScheme.background.toArgb()
|
||||
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = !isDarkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package org.lsposed.lspatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package org.lsposed.lspatch.ui.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
|
||||
val LocalNavController = compositionLocalOf<NavHostController> {
|
||||
error("CompositionLocal LocalNavController not present")
|
||||
}
|
||||
|
||||
val NavController.currentRoute: String?
|
||||
@Composable get() = currentBackStackEntryAsState().value?.destination?.route
|
||||
|
||||
val NavController.startRoute: String?
|
||||
get() = graph.findStartDestination().route
|
||||
|
||||
@Composable
|
||||
fun NavController.isAtStartRoute(): Boolean = currentRoute == startRoute
|
||||
|
||||
fun NavController.navigateWithState(route: String?) {
|
||||
navigate(route.toString()) {
|
||||
popUpTo(startRoute.toString()) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package org.lsposed.lspatch.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.lsposed.lspatch.TAG
|
||||
|
||||
class AppInfo(val app: ApplicationInfo, val icon: Drawable, val label: String)
|
||||
|
||||
class SelectAppViewModel : ViewModel() {
|
||||
|
||||
init {
|
||||
Log.d(TAG, "SelectAppViewModel ${toString().substringAfterLast('@')} construct")
|
||||
}
|
||||
|
||||
companion object {
|
||||
var appList by mutableStateOf(listOf<AppInfo>())
|
||||
private set
|
||||
}
|
||||
|
||||
private val _isRefreshing = MutableStateFlow(false)
|
||||
val isRefreshing: StateFlow<Boolean>
|
||||
get() = _isRefreshing.asStateFlow()
|
||||
|
||||
fun loadAppList(context: Context) {
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "Start refresh apps")
|
||||
_isRefreshing.emit(true)
|
||||
val pm = context.packageManager
|
||||
val collection = mutableListOf<AppInfo>()
|
||||
withContext(Dispatchers.IO) {
|
||||
pm.getInstalledApplications(PackageManager.GET_META_DATA).forEach {
|
||||
val icon = pm.getApplicationIcon(it)
|
||||
val label = pm.getApplicationLabel(it)
|
||||
collection.add(AppInfo(it, icon, label.toString()))
|
||||
}
|
||||
}
|
||||
appList = collection
|
||||
_isRefreshing.emit(false)
|
||||
Log.d(TAG, "Refreshed ${appList.size} apps")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.LSPatch" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -1,3 +1,21 @@
|
|||
<resources>
|
||||
<string name="app_name">LSPatch</string>
|
||||
</resources>
|
||||
|
||||
<string name="page_repo">Repo</string>
|
||||
<string name="page_settings">Settings</string>
|
||||
<string name="page_select_app">Select App</string>
|
||||
|
||||
<!-- Patches Page -->
|
||||
<string name="page_patches">Patches</string>
|
||||
<string name="patches_add">Add</string>
|
||||
|
||||
<!-- New Patch Page -->
|
||||
<string name="page_new_patch">New Patch</string>
|
||||
<string name="patch_mode">Patch Mode</string>
|
||||
<string name="patch_local">Local</string>
|
||||
<string name="patch_local_desc">Patch an app without modules embedded.\nThe patched app need the manager running in background, and Xposed scope can be changed dynamically without re-patch.\nLocal patched apps can only run on the local device.</string>
|
||||
<string name="patch_portable">Portable</string>
|
||||
<string name="patch_portable_desc">Patch an app with modules embedded.\nThe patched app can run without the manager, but cannot be managed dynamically.\nPortable patched apps can be used on devices that do not have LSPatch Manager installed.</string>
|
||||
<string name="patch_embed_modules">Embed modules</string>
|
||||
<string name="patch_start">Start Patch</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.LSPatch" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
Loading…
Reference in New Issue