config refactor + ui

This commit is contained in:
rhunk 2023-08-01 18:37:35 +02:00
parent e2249417eb
commit 79d3bb5ba9
76 changed files with 390 additions and 1640 deletions

View File

@ -50,16 +50,6 @@
android:name=".ui.map.MapActivity"
android:exported="true"
android:excludeFromRecents="true" />
<activity
android:name=".ui.config.ConfigActivity"
android:theme="@style/AppTheme"
android:excludeFromRecents="true"
android:exported="true" />
<activity
android:name=".ui.spoof.DeviceSpooferActivity"
android:theme="@style/AppTheme"
android:excludeFromRecents="true"
android:exported="true" />
<activity android:name=".bridge.ForceStartActivity"
android:theme="@android:style/Theme.NoDisplay"
android:excludeFromRecents="true"

View File

@ -1,15 +1,12 @@
package me.rhunk.snapenhance.manager
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.navigation.compose.rememberNavController
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.manager.data.ManagerContext
import me.rhunk.snapenhance.manager.util.SaveFolderChecker
import me.rhunk.snapenhance.util.ActivityResultCallback

View File

@ -20,7 +20,6 @@ import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import me.rhunk.snapenhance.manager.data.ManagerContext
@ -36,16 +35,15 @@ class Navigation(
) {
val sections = remember { EnumSection.values().toList().map {
it to it.section.constructors.first().call()
}.onEach { (_, instance) ->
}.onEach { (section, instance) ->
instance.enumSection = section
instance.manager = context
instance.navController = navController
} }
NavHost(navController, startDestination = startDestination.route, Modifier.padding(innerPadding)) {
sections.forEach { (section, instance) ->
composable(section.route) {
instance.Content()
}
sections.forEach { (_, instance) ->
instance.build(this)
}
}
}

View File

@ -9,10 +9,12 @@ import androidx.compose.material.icons.filled.Stars
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import me.rhunk.snapenhance.manager.data.ManagerContext
import me.rhunk.snapenhance.manager.sections.FeaturesSection
import me.rhunk.snapenhance.manager.sections.HomeSection
import me.rhunk.snapenhance.manager.sections.NotImplemented
import me.rhunk.snapenhance.manager.sections.features.FeaturesSection
import kotlin.reflect.KClass
enum class EnumSection(
@ -59,9 +61,16 @@ enum class EnumSection(
open class Section {
lateinit var enumSection: EnumSection
lateinit var manager: ManagerContext
lateinit var navController: NavController
@Composable
open fun Content() { NotImplemented() }
open fun build(navGraphBuilder: NavGraphBuilder) {
navGraphBuilder.composable(enumSection.route) {
Content()
}
}
}

View File

@ -1,7 +1,6 @@
package me.rhunk.snapenhance.manager.data
import android.content.Context
import me.rhunk.snapenhance.bridge.wrapper.ConfigWrapper
import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper
import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper
import me.rhunk.snapenhance.core.config.ModConfig

View File

@ -1,235 +0,0 @@
package me.rhunk.snapenhance.manager.sections
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.SnackbarHost
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.rounded.Save
import androidx.compose.material.rememberScaffoldState
import androidx.compose.material3.Card
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.config.impl.ConfigIntegerValue
import me.rhunk.snapenhance.config.impl.ConfigStateListValue
import me.rhunk.snapenhance.config.impl.ConfigStateSelection
import me.rhunk.snapenhance.config.impl.ConfigStateValue
import me.rhunk.snapenhance.config.impl.ConfigStringValue
import me.rhunk.snapenhance.manager.Dialogs
import me.rhunk.snapenhance.manager.Section
typealias ClickCallback = (Boolean) -> Unit
typealias RegisterClickCallback = (ClickCallback) -> ClickCallback
class FeaturesSection : Section() {
private val dialogs by lazy { Dialogs(manager) }
@Composable
private fun PropertyAction(item: ConfigProperty, registerClickCallback: RegisterClickCallback) {
val showDialog = remember { mutableStateOf(false) }
val dialogComposable = remember { mutableStateOf<@Composable () -> Unit>({})}
fun registerDialogOnClickCallback() = registerClickCallback {
showDialog.value = true
}
if (showDialog.value) {
Dialog(onDismissRequest = { showDialog.value = false }, properties = DialogProperties()) {
dialogComposable.value()
}
}
when (val container = remember { item.valueContainer }) {
is ConfigStateValue -> {
val state = remember { mutableStateOf(container.value()) }
Switch(
checked = state.value,
onCheckedChange = registerClickCallback {
state.value = state.value.not()
container.writeFrom(state.value.toString())
}
)
}
is ConfigStateSelection -> {
registerDialogOnClickCallback()
dialogComposable.value = {
dialogs.StateSelectionDialog(item)
}
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.widthIn(0.dp, 120.dp),
text = if (item.disableValueLocalization) container.value() else {
manager.translation.propertyOption(item, container.value())
},
)
}
is ConfigStateListValue, is ConfigStringValue, is ConfigIntegerValue -> {
dialogComposable.value = {
when (container) {
is ConfigStateListValue -> {
dialogs.StateListDialog(item)
}
is ConfigStringValue, is ConfigIntegerValue -> {
dialogs.KeyboardInputDialog(item) { showDialog.value = false }
}
}
}
registerDialogOnClickCallback().let { { it.invoke(true) } }.also {
if (container is ConfigIntegerValue) {
FilledIconButton(onClick = it) {
Text(text = container.value().toString(), modifier = Modifier.wrapContentWidth(), overflow = TextOverflow.Ellipsis)
}
} else {
IconButton(onClick = it) {
Icon(Icons.Filled.OpenInNew, contentDescription = null)
}
}
}
}
}
}
@Composable
private fun PropertyCard(item: ConfigProperty) {
val clickCallback = remember { mutableStateOf<ClickCallback?>(null) }
Card(
modifier = Modifier
.fillMaxWidth()
.clickable {
clickCallback.value?.invoke(true)
}
.padding(start = 10.dp, end = 10.dp, top = 5.dp, bottom = 5.dp)
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(all = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f, fill = true)
.padding(all = 10.dp)
) {
Text(text = manager.translation.propertyName(item), fontSize = 16.sp, fontWeight = FontWeight.Bold)
Text(
text = manager.translation.propertyDescription(item),
fontSize = 12.sp,
lineHeight = 15.sp
)
}
Column(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(all = 10.dp)
) {
PropertyAction(item, registerClickCallback = { callback ->
clickCallback.value = callback
callback
})
}
}
}
}
@Composable
private fun PropertyContainer() {
val properties = remember {
val items by manager.config
items.properties.map { it.key to it.value }
}
LazyColumn(
modifier = Modifier
.fillMaxHeight(),
verticalArrangement = Arrangement.Center
) {
items(properties) { (key, value) ->
// Logger.debug("key: $key, value: $value")
}
}
}
@Composable
@Preview
override fun Content() {
val scope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
Scaffold(
snackbarHost = { SnackbarHost(scaffoldState.snackbarHostState) },
floatingActionButton = {
FloatingActionButton(
onClick = {
//manager.config.writeConfig()
scope.launch {
scaffoldState.snackbarHostState.showSnackbar("Saved")
}
},
containerColor = MaterialTheme.colors.primary,
contentColor = MaterialTheme.colors.onPrimary,
shape = RoundedCornerShape(16.dp),
) {
Icon(
imageVector = Icons.Rounded.Save,
contentDescription = null
)
}
},
modifier = Modifier.fillMaxSize(),
content = { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
Text(
text = "Features",
modifier = Modifier.padding(all = 10.dp),
fontSize = 20.sp
)
PropertyContainer()
}
}
)
}
}

View File

@ -0,0 +1,4 @@
package me.rhunk.snapenhance.manager.sections.features
typealias ClickCallback = (Boolean) -> Unit
typealias RegisterClickCallback = (ClickCallback) -> ClickCallback

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.manager
package me.rhunk.snapenhance.manager.sections.features
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
@ -29,16 +29,11 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.config.impl.ConfigIntegerValue
import me.rhunk.snapenhance.config.impl.ConfigStateListValue
import me.rhunk.snapenhance.config.impl.ConfigStateSelection
import me.rhunk.snapenhance.manager.data.ManagerContext
import me.rhunk.snapenhance.core.config.DataProcessors
import me.rhunk.snapenhance.core.config.PropertyPair
class Dialogs(
private val context: ManagerContext
) {
class Dialogs {
@Composable
fun DefaultDialogCard(content: @Composable ColumnScope.() -> Unit) {
Card(
@ -65,17 +60,25 @@ class Dialogs(
}
@Composable
fun StateSelectionDialog(config: ConfigProperty) {
assert(config.valueContainer is ConfigStateSelection)
val keys = (config.valueContainer as ConfigStateSelection).keys()
val selectedValue = remember {
mutableStateOf(config.valueContainer.value())
@Suppress("UNCHECKED_CAST")
fun UniqueSelectionDialog(property: PropertyPair<*>) {
val keys = (property.value.defaultValues as List<String>).toMutableList().apply {
add(0, "disabled")
}
val selectedValue = remember {
mutableStateOf(property.value.getNullable()?.toString() ?: "disabled")
}
DefaultDialogCard {
keys.forEach { item ->
keys.forEachIndexed { index, item ->
fun select() {
selectedValue.value = item
config.valueContainer.writeFrom(item)
property.value.setAny(if (index == 0) {
null
} else {
item
})
}
Row(
@ -83,9 +86,7 @@ class Dialogs(
verticalAlignment = Alignment.CenterVertically
) {
DefaultEntryText(
text = if (config.disableValueLocalization)
item
else context.translation.propertyOption(config, item),
text = item,
modifier = Modifier.weight(1f)
)
RadioButton(
@ -98,12 +99,12 @@ class Dialogs(
}
@Composable
fun KeyboardInputDialog(config: ConfigProperty, dismiss: () -> Unit = {}) {
fun KeyboardInputDialog(property: PropertyPair<*>, dismiss: () -> Unit = {}) {
val focusRequester = remember { FocusRequester() }
DefaultDialogCard {
val fieldValue = remember {
mutableStateOf(config.valueContainer.read().let {
mutableStateOf(property.value.get().toString().let {
TextFieldValue(
text = it,
selection = TextRange(it.length)
@ -123,8 +124,8 @@ class Dialogs(
onValueChange = {
fieldValue.value = it
},
keyboardOptions = when (config.valueContainer) {
is ConfigIntegerValue -> {
keyboardOptions = when (property.key.dataType.type) {
DataProcessors.Type.INTEGER -> {
KeyboardOptions(keyboardType = KeyboardType.Number)
}
else -> {
@ -142,7 +143,15 @@ class Dialogs(
Text(text = "Cancel")
}
Button(onClick = {
config.valueContainer.writeFrom(fieldValue.value.text)
if (property.key.dataType.type == DataProcessors.Type.INTEGER) {
runCatching {
property.value.setAny(fieldValue.value.text.toInt())
}.onFailure {
property.value.setAny(0)
}
} else {
property.value.setAny(fieldValue.value.text)
}
dismiss()
}) {
Text(text = "Ok")
@ -152,18 +161,23 @@ class Dialogs(
}
@Composable
fun StateListDialog(config: ConfigProperty) {
assert(config.valueContainer is ConfigStateListValue)
val stateList = (config.valueContainer as ConfigStateListValue).value()
@Suppress("UNCHECKED_CAST")
fun MultipleSelectionDialog(property: PropertyPair<*>) {
val defaultItems = property.value.defaultValues as List<String>
val toggledStates = property.value.get() as MutableList<String>
DefaultDialogCard {
stateList.keys.forEach { key ->
defaultItems.forEach { key ->
val state = remember {
mutableStateOf(stateList[key] ?: false)
mutableStateOf(toggledStates.contains(key))
}
fun toggle(value: Boolean? = null) {
state.value = value ?: !state.value
stateList[key] = state.value
if (state.value) {
toggledStates.add(key)
} else {
toggledStates.remove(key)
}
}
Row(
@ -171,9 +185,7 @@ class Dialogs(
verticalAlignment = Alignment.CenterVertically
) {
DefaultEntryText(
text = if (config.disableValueLocalization)
key
else context.translation.propertyOption(config, key),
text = key,
modifier = Modifier
.weight(1f)
)

View File

@ -0,0 +1,293 @@
package me.rhunk.snapenhance.manager.sections.features
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.rounded.Save
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.core.config.ConfigContainer
import me.rhunk.snapenhance.core.config.DataProcessors
import me.rhunk.snapenhance.core.config.PropertyPair
import me.rhunk.snapenhance.manager.Section
class FeaturesSection : Section() {
private val dialogs by lazy { Dialogs() }
companion object {
private const val MAIN_ROUTE = "root"
}
@Composable
private fun PropertyAction(property: PropertyPair<*>, registerClickCallback: RegisterClickCallback) {
val showDialog = remember { mutableStateOf(false) }
val dialogComposable = remember { mutableStateOf<@Composable () -> Unit>({}) }
fun registerDialogOnClickCallback() = registerClickCallback {
showDialog.value = true
}
if (showDialog.value) {
Dialog(
onDismissRequest = { showDialog.value = false },
properties = DialogProperties()
) {
dialogComposable.value()
}
}
val propertyValue = property.value
when (val dataType = remember { property.key.dataType.type }) {
DataProcessors.Type.BOOLEAN -> {
val state = remember { mutableStateOf(propertyValue.get() as Boolean) }
Switch(
checked = state.value,
onCheckedChange = registerClickCallback {
state.value = state.value.not()
propertyValue.setAny(state.value)
}
)
}
DataProcessors.Type.STRING_UNIQUE_SELECTION -> {
registerDialogOnClickCallback()
dialogComposable.value = {
dialogs.UniqueSelectionDialog(property)
}
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.widthIn(0.dp, 120.dp),
text = (propertyValue.getNullable() as? String) ?: "Disabled",
)
}
DataProcessors.Type.STRING_MULTIPLE_SELECTION, DataProcessors.Type.STRING, DataProcessors.Type.INTEGER -> {
dialogComposable.value = {
when (dataType) {
DataProcessors.Type.STRING_MULTIPLE_SELECTION -> {
dialogs.MultipleSelectionDialog(property)
}
DataProcessors.Type.STRING, DataProcessors.Type.INTEGER -> {
dialogs.KeyboardInputDialog(property) { showDialog.value = false }
}
else -> {}
}
}
registerDialogOnClickCallback().let { { it.invoke(true) } }.also {
if (dataType == DataProcessors.Type.INTEGER) {
FilledIconButton(onClick = it) {
Text(
text = propertyValue.get().toString(),
modifier = Modifier.wrapContentWidth(),
overflow = TextOverflow.Ellipsis
)
}
} else {
IconButton(onClick = it) {
Icon(Icons.Filled.OpenInNew, contentDescription = null)
}
}
}
}
else -> {}
}
}
@Composable
private fun PropertyCard(property: PropertyPair<*>) {
val clickCallback = remember { mutableStateOf<ClickCallback?>(null) }
Card(
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 5.dp, bottom = 5.dp)
) {
Row(
modifier = Modifier
.fillMaxSize()
.clickable {
clickCallback.value?.invoke(true)
}
.padding(all = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f, fill = true)
.padding(all = 10.dp)
) {
Text(
text = property.name,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
Text(
text = property.name,
fontSize = 12.sp,
lineHeight = 15.sp
)
}
when (property.key.dataType.type) {
DataProcessors.Type.CONTAINER -> {
clickCallback.value = {
navController.navigate("container/${property.name}")
}
}
else -> {
Column(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(all = 10.dp)
) {
PropertyAction(property, registerClickCallback = { callback ->
clickCallback.value = callback
callback
})
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Container(
containerName: String,
configContainer: ConfigContainer
) {
val properties = remember {
configContainer.properties.map { PropertyPair(it.key, it.value) }
}
val scope = rememberCoroutineScope()
val scaffoldState = rememberBottomSheetScaffoldState()
Scaffold(
snackbarHost = { SnackbarHost(scaffoldState.snackbarHostState) },
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = {
Text(text = containerName, textAlign = TextAlign.Center)
},
navigationIcon = {
if (navController.currentBackStackEntry?.destination?.route != MAIN_ROUTE) {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Filled.ArrowBack, contentDescription = null)
}
}
}
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
manager.config.writeConfig()
scope.launch {
scaffoldState.snackbarHostState.showSnackbar("Saved")
}
},
modifier = Modifier.padding(25.dp),
containerColor = MaterialTheme.colors.primary,
contentColor = MaterialTheme.colors.onPrimary,
shape = RoundedCornerShape(16.dp),
) {
Icon(
imageVector = Icons.Rounded.Save,
contentDescription = null
)
}
},
content = { innerPadding ->
LazyColumn(
modifier = Modifier.fillMaxHeight().padding(innerPadding),
verticalArrangement = Arrangement.Center
) {
items(properties) {
PropertyCard(it)
}
}
}
)
}
override fun build(navGraphBuilder: NavGraphBuilder) {
val allContainers by lazy {
val containers = mutableMapOf<String, ConfigContainer>()
fun queryContainerRecursive(container: ConfigContainer) {
container.properties.forEach {
if (it.key.dataType.type == DataProcessors.Type.CONTAINER) {
containers[it.key.name] = it.value.get() as ConfigContainer
queryContainerRecursive(it.value.get() as ConfigContainer)
}
}
}
queryContainerRecursive(manager.config.root)
containers
}
navGraphBuilder.navigation(route = "features", startDestination = MAIN_ROUTE) {
composable(MAIN_ROUTE) {
Container(MAIN_ROUTE, manager.config.root)
}
composable("container/{name}") { backStackEntry ->
backStackEntry.arguments?.getString("name")?.let { containerName ->
allContainers[containerName]?.let {
Container(containerName, it)
}
}
}
}
}
}

View File

@ -6,7 +6,6 @@ import android.content.Intent
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.core.config.PropertyValue
import kotlin.system.exitProcess

View File

@ -14,7 +14,6 @@ import kotlinx.coroutines.asCoroutineDispatcher
import me.rhunk.snapenhance.bridge.BridgeClient
import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper
import me.rhunk.snapenhance.core.config.ModConfig
import me.rhunk.snapenhance.core.config.impl.RootConfig
import me.rhunk.snapenhance.data.MessageSender
import me.rhunk.snapenhance.database.DatabaseAccess
import me.rhunk.snapenhance.features.Feature

View File

@ -7,7 +7,6 @@ import android.content.Intent
import android.os.Build
import android.os.Environment
import android.provider.Settings
import me.rhunk.snapenhance.bridge.wrapper.ConfigWrapper
import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper
import me.rhunk.snapenhance.download.DownloadTaskManager
import kotlin.system.exitProcess
@ -18,7 +17,6 @@ import kotlin.system.exitProcess
object SharedContext {
lateinit var downloadTaskManager: DownloadTaskManager
lateinit var translation: TranslationWrapper
lateinit var config: ConfigWrapper
private fun askForStoragePermission(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@ -76,10 +74,6 @@ object SharedContext {
loadFromContext(context)
}
}
if (!this::config.isInitialized) {
config = ConfigWrapper().apply { loadFromContext(context) }
}
//askForPermissions(context)
}
}

View File

@ -1,12 +1,10 @@
package me.rhunk.snapenhance.action
import me.rhunk.snapenhance.ModContext
import me.rhunk.snapenhance.config.ConfigProperty
import java.io.File
abstract class AbstractAction(
val nameKey: String,
val dependsOnProperty: ConfigProperty? = null,
val nameKey: String
) {
lateinit var context: ModContext

View File

@ -1,10 +1,9 @@
package me.rhunk.snapenhance.action.impl
import me.rhunk.snapenhance.action.AbstractAction
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.impl.AutoUpdater
class CheckForUpdates : AbstractAction("action.check_for_updates", dependsOnProperty = ConfigProperty.AUTO_UPDATER) {
class CheckForUpdates : AbstractAction("action.check_for_updates") {
override fun run() {
context.executeAsync {
runCatching {

View File

@ -3,11 +3,10 @@ package me.rhunk.snapenhance.action.impl
import android.content.Intent
import android.os.Bundle
import me.rhunk.snapenhance.action.AbstractAction
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.ui.map.MapActivity
class OpenMap: AbstractAction("action.open_map", dependsOnProperty = ConfigProperty.LOCATION_SPOOF) {
class OpenMap: AbstractAction("action.open_map") {
override fun run() {
context.runOnUiThread {
val mapActivityIntent = Intent()

View File

@ -10,10 +10,10 @@ import android.os.Handler
import android.os.HandlerThread
import android.os.IBinder
import de.robv.android.xposed.XposedHelpers
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.Logger.xposedLog
import me.rhunk.snapenhance.ModContext
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.data.LocalePair
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors

View File

@ -1,73 +0,0 @@
package me.rhunk.snapenhance.bridge.wrapper
import android.content.Context
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.bridge.BridgeClient
import me.rhunk.snapenhance.bridge.FileLoaderWrapper
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.config.ConfigAccessor
import me.rhunk.snapenhance.config.ConfigProperty
class ConfigWrapper: ConfigAccessor() {
companion object {
private val gson = GsonBuilder().setPrettyPrinting().create()
}
private val file = FileLoaderWrapper(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8))
fun load() {
ConfigProperty.sortedByCategory().forEach { key ->
set(key, key.valueContainer)
}
if (!file.isFileExists()) {
writeConfig()
return
}
runCatching {
loadConfig()
}.onFailure {
Logger.error("Failed to load config", it)
writeConfig()
}
}
fun save() {
writeConfig()
}
private fun loadConfig() {
val configContent = file.read()
val configObject: JsonObject = gson.fromJson(
configContent.toString(Charsets.UTF_8),
JsonObject::class.java
)
entries().forEach { (key, value) ->
value.writeFrom(configObject.get(key.name)?.asString ?: value.read())
}
}
fun writeConfig() {
val configObject = JsonObject()
entries().forEach { (key, value) ->
configObject.addProperty(key.name, value.read())
}
file.write(gson.toJson(configObject).toByteArray(Charsets.UTF_8))
}
fun loadFromContext(context: Context) {
file.loadFromContext(context)
load()
}
fun loadFromBridge(bridgeClient: BridgeClient) {
file.loadFromBridge(bridgeClient)
load()
}
}

View File

@ -5,7 +5,6 @@ import com.google.gson.JsonObject
import com.google.gson.JsonParser
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.bridge.BridgeClient
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.LocalePair
import java.util.Locale
@ -80,18 +79,6 @@ class TranslationWrapper {
return translationMap[key] ?: key.also { Logger.debug("Missing translation for $key") }
}
fun propertyName(property: ConfigProperty): String {
return get("property.${property.translationKey}.name")
}
fun propertyDescription(property: ConfigProperty): String {
return get("property.${property.translationKey}.description")
}
fun propertyOption(property: ConfigProperty, item: String): String {
return get(property.getOptionTranslationKey(item))
}
fun format(key: String, vararg args: Pair<String, String>): String {
return args.fold(get(key)) { acc, pair ->
acc.replace("{${pair.first}}", pair.second)

View File

@ -1,62 +0,0 @@
package me.rhunk.snapenhance.config
open class ConfigAccessor(
private val configMap: MutableMap<ConfigProperty, ConfigValue<*>> = mutableMapOf()
) {
fun bool(key: ConfigProperty): Boolean {
return get(key).value() as Boolean
}
fun int(key: ConfigProperty): Int {
return get(key).value() as Int
}
fun string(key: ConfigProperty): String {
return get(key).value() as String
}
fun double(key: ConfigProperty): Double {
return get(key).value() as Double
}
fun float(key: ConfigProperty): Float {
return get(key).value() as Float
}
fun long(key: ConfigProperty): Long {
return get(key).value() as Long
}
fun short(key: ConfigProperty): Short {
return get(key).value() as Short
}
fun byte(key: ConfigProperty): Byte {
return get(key).value() as Byte
}
fun char(key: ConfigProperty): Char {
return get(key).value() as Char
}
@Suppress("UNCHECKED_CAST")
fun options(key: ConfigProperty): Map<String, Boolean> {
return get(key).value() as Map<String, Boolean>
}
fun state(key: ConfigProperty): String {
return get(key).value() as String
}
fun get(key: ConfigProperty): ConfigValue<*> {
return configMap[key]!!
}
fun set(key: ConfigProperty, value: ConfigValue<*>) {
configMap[key] = value
}
fun entries(): Set<Map.Entry<ConfigProperty, ConfigValue<*>>> {
return configMap.entries
}
}

View File

@ -1,14 +0,0 @@
package me.rhunk.snapenhance.config
enum class ConfigCategory(
val key: String,
val hidden: Boolean = false
) {
SPYING_PRIVACY("spying_privacy"),
MEDIA_MANAGEMENT("media_manager"),
UI_TWEAKS("ui_tweaks"),
UPDATES("updates"),
CAMERA("camera"),
EXPERIMENTAL_DEBUGGING("experimental_debugging"),
DEVICE_SPOOFER("device_spoofer", hidden = true)
}

View File

@ -1,417 +0,0 @@
package me.rhunk.snapenhance.config
import me.rhunk.snapenhance.config.impl.ConfigIntegerValue
import me.rhunk.snapenhance.config.impl.ConfigStateListValue
import me.rhunk.snapenhance.config.impl.ConfigStateSelection
import me.rhunk.snapenhance.config.impl.ConfigStateValue
import me.rhunk.snapenhance.config.impl.ConfigStringValue
import me.rhunk.snapenhance.data.NotificationType
import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks
enum class ConfigProperty(
val translationKey: String,
val category: ConfigCategory,
val valueContainer: ConfigValue<*>,
val valueContainerTranslationKey: String? = null,
val shouldAppearInSettings: Boolean = true,
val disableValueLocalization: Boolean = false
) {
//SPYING AND PRIVACY
MESSAGE_LOGGER("message_logger",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
PREVENT_READ_RECEIPTS(
"prevent_read_receipts",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
HIDE_BITMOJI_PRESENCE(
"hide_bitmoji_presence",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
BETTER_NOTIFICATIONS(
"better_notifications",
ConfigCategory.SPYING_PRIVACY,
ConfigStateListValue(
listOf("snap", "chat", "reply_button", "download_button"),
mutableMapOf(
"snap" to false,
"chat" to false,
"reply_button" to false,
"download_button" to false
)
)
),
NOTIFICATION_BLACKLIST(
"notification_blacklist",
ConfigCategory.SPYING_PRIVACY,
ConfigStateListValue(
NotificationType.getIncomingValues().map { it.key },
NotificationType.getIncomingValues().associate { it.key to false }.toMutableMap()
),
valueContainerTranslationKey = "notifications",
),
DISABLE_METRICS("disable_metrics",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
BLOCK_ADS("block_ads",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
UNLIMITED_SNAP_VIEW_TIME("unlimited_snap_view_time",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
PREVENT_SENDING_MESSAGES(
"prevent_sending_messages",
ConfigCategory.SPYING_PRIVACY,
ConfigStateListValue(
NotificationType.getOutgoingValues().map { it.key },
NotificationType.getOutgoingValues().associate { it.key to false }.toMutableMap()
),
valueContainerTranslationKey = "notifications",
),
ANONYMOUS_STORY_VIEW(
"anonymous_story_view",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
HIDE_TYPING_NOTIFICATION(
"hide_typing_notification",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
//MEDIA MANAGEMENT
SAVE_FOLDER(
"save_folder",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStringValue("", isFolderPath =true),
),
AUTO_DOWNLOAD_OPTIONS(
"auto_download_options",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateListValue(
listOf("friend_snaps", "friend_stories", "public_stories", "spotlight"),
mutableMapOf(
"friend_snaps" to false,
"friend_stories" to false,
"public_stories" to false,
"spotlight" to false
)
)
),
DOWNLOAD_OPTIONS(
"download_options",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateListValue(
listOf(
"allow_duplicate",
"create_user_folder",
"append_hash",
"append_date_time",
"append_type",
"append_username",
"merge_overlay"
),
mutableMapOf(
"allow_duplicate" to false,
"create_user_folder" to true,
"append_hash" to true,
"append_date_time" to true,
"append_type" to false,
"append_username" to false,
"merge_overlay" to false,
)
)
),
CHAT_DOWNLOAD_CONTEXT_MENU(
"chat_download_context_menu",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateValue(false)
),
GALLERY_MEDIA_SEND_OVERRIDE(
"gallery_media_send_override",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateSelection(
listOf("OFF", "NOTE", "SNAP", "LIVE_SNAP"),
"OFF"
)
),
AUTO_SAVE_MESSAGES("auto_save_messages",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateListValue(
listOf("CHAT", "SNAP", "NOTE", "EXTERNAL_MEDIA", "STICKER")
)
),
FORCE_MEDIA_SOURCE_QUALITY(
"force_media_source_quality",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateValue(false)
),
DOWNLOAD_LOGGING(
"download_logging",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateListValue(
listOf("started", "success", "progress", "failure"),
mutableMapOf(
"started" to false,
"success" to true,
"progress" to false,
"failure" to true
)
)
),
//UI AND TWEAKS
ENABLE_FRIEND_FEED_MENU_BAR(
"enable_friend_feed_menu_bar",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
FRIEND_FEED_MENU_BUTTONS(
"friend_feed_menu_buttons",
ConfigCategory.UI_TWEAKS,
ConfigStateListValue(
listOf("auto_download_blacklist", "anti_auto_save", "stealth_mode", "conversation_info"),
mutableMapOf(
"auto_download_blacklist" to false,
"anti_auto_save" to false,
"stealth_mode" to true,
"conversation_info" to true
)
)
),
FRIEND_FEED_MENU_POSITION("friend_feed_menu_buttons_position",
ConfigCategory.UI_TWEAKS,
ConfigIntegerValue(1)
),
HIDE_UI_ELEMENTS(
"hide_ui_elements",
ConfigCategory.UI_TWEAKS,
ConfigStateListValue(
listOf("remove_voice_record_button", "remove_stickers_button", "remove_cognac_button", "remove_live_location_share_button", "remove_call_buttons", "remove_camera_borders"),
mutableMapOf(
"remove_voice_record_button" to false,
"remove_stickers_button" to false,
"remove_cognac_button" to false,
"remove_live_location_share_button" to false,
"remove_call_buttons" to false,
"remove_camera_borders" to false
)
)
),
HIDE_STORY_SECTION(
"hide_story_section",
ConfigCategory.UI_TWEAKS,
ConfigStateListValue(
listOf("hide_friend_suggestions", "hide_friends", "hide_following", "hide_for_you"),
mutableMapOf(
"hide_friend_suggestions" to false,
"hide_friends" to false,
"hide_following" to false,
"hide_for_you" to false
)
)
),
STORY_VIEWER_OVERRIDE("story_viewer_override",
ConfigCategory.UI_TWEAKS,
ConfigStateSelection(
listOf("OFF", "DISCOVER_PLAYBACK_SEEKBAR", "VERTICAL_STORY_VIEWER"),
"OFF"
)
),
STREAK_EXPIRATION_INFO(
"streak_expiration_info",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
DISABLE_SNAP_SPLITTING(
"disable_snap_splitting",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
DISABLE_VIDEO_LENGTH_RESTRICTION(
"disable_video_length_restriction",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
SNAPCHAT_PLUS("snapchat_plus",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
NEW_MAP_UI("new_map_ui",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
LOCATION_SPOOF(
"location_spoof",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
LATITUDE(
"latitude_value",
ConfigCategory.UI_TWEAKS,
ConfigStringValue("0.0000"),
shouldAppearInSettings = false
),
LONGITUDE(
"longitude_value",
ConfigCategory.UI_TWEAKS,
ConfigStringValue("0.0000"),
shouldAppearInSettings = false
),
MESSAGE_PREVIEW_LENGTH(
"message_preview_length",
ConfigCategory.UI_TWEAKS,
ConfigIntegerValue(20)
),
UNLIMITED_CONVERSATION_PINNING(
"unlimited_conversation_pinning",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
DISABLE_SPOTLIGHT(
"disable_spotlight",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
ENABLE_APP_APPEARANCE(
"enable_app_appearance",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
STARTUP_PAGE_OVERRIDE(
"startup_page_override",
ConfigCategory.UI_TWEAKS,
ConfigStateSelection(
listOf(
"OFF",
"ngs_map_icon_container",
"ngs_chat_icon_container",
"ngs_camera_icon_container",
"ngs_community_icon_container",
"ngs_spotlight_icon_container",
"ngs_search_icon_container"
),
"OFF"
)
),
DISABLE_GOOGLE_PLAY_DIALOGS(
"disable_google_play_dialogs",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
//CAMERA
CAMERA_DISABLE(
"disable_camera",
ConfigCategory.CAMERA,
ConfigStateValue(false)
),
IMMERSIVE_CAMERA_PREVIEW(
"immersive_camera_preview",
ConfigCategory.CAMERA,
ConfigStateValue(false)
),
OVERRIDE_PREVIEW_RESOLUTION(
"preview_resolution",
ConfigCategory.CAMERA,
ConfigStateSelection(
CameraTweaks.resolutions,
"OFF"
),
disableValueLocalization = true
),
OVERRIDE_PICTURE_RESOLUTION(
"picture_resolution",
ConfigCategory.CAMERA,
ConfigStateSelection(
CameraTweaks.resolutions,
"OFF"
),
disableValueLocalization = true
),
FORCE_HIGHEST_FRAME_RATE(
"force_highest_frame_rate",
ConfigCategory.CAMERA,
ConfigStateValue(false)
),
FORCE_CAMERA_SOURCE_ENCODING(
"force_camera_source_encoding",
ConfigCategory.CAMERA,
ConfigStateValue(false)
),
// UPDATES
AUTO_UPDATER(
"auto_updater",
ConfigCategory.UPDATES,
ConfigStateSelection(
listOf("DISABLED", "EVERY_LAUNCH", "DAILY", "WEEKLY"),
"DAILY"
)
),
// EXPERIMENTAL DEBUGGING
APP_PASSCODE(
"app_passcode",
ConfigCategory.EXPERIMENTAL_DEBUGGING,
ConfigStringValue("", isHidden = true)
),
APP_LOCK_ON_RESUME(
"app_lock_on_resume",
ConfigCategory.EXPERIMENTAL_DEBUGGING,
ConfigStateValue(false)
),
INFINITE_STORY_BOOST(
"infinite_story_boost",
ConfigCategory.EXPERIMENTAL_DEBUGGING,
ConfigStateValue(false)
),
MEO_PASSCODE_BYPASS(
"meo_passcode_bypass",
ConfigCategory.EXPERIMENTAL_DEBUGGING,
ConfigStateValue(false)
),
AMOLED_DARK_MODE(
"amoled_dark_mode",
ConfigCategory.EXPERIMENTAL_DEBUGGING,
ConfigStateValue(false)
),
UNLIMITED_MULTI_SNAP(
"unlimited_multi_snap",
ConfigCategory.EXPERIMENTAL_DEBUGGING,
ConfigStateValue(false)
),
//DEVICE SPOOFER
DEVICE_SPOOF(
"device_spoof",
ConfigCategory.DEVICE_SPOOFER,
ConfigStateValue(false)
),
FINGERPRINT(
"device_fingerprint",
ConfigCategory.DEVICE_SPOOFER,
ConfigStringValue("")
),
ANDROID_ID(
"android_id",
ConfigCategory.DEVICE_SPOOFER,
ConfigStringValue("")
);
fun getOptionTranslationKey(key: String) = "option.property.${valueContainerTranslationKey ?: translationKey}.$key"
companion object {
fun sortedByCategory(): List<ConfigProperty> {
return values().sortedBy { it.category.ordinal }
}
}
}

View File

@ -1,22 +0,0 @@
package me.rhunk.snapenhance.config
abstract class ConfigValue<T> {
private val propertyChangeListeners = mutableListOf<(T) -> Unit>()
fun addPropertyChangeListener(listener: (T) -> Unit) = propertyChangeListeners.add(listener)
fun removePropertyChangeListener(listener: (T) -> Unit) = propertyChangeListeners.remove(listener)
abstract fun value(): T
abstract fun read(): String
protected abstract fun write(value: String)
protected fun onValueChanged() {
propertyChangeListeners.forEach { it(value()) }
}
fun writeFrom(value: String) {
val oldValue = read()
write(value)
if (oldValue != value) onValueChanged()
}
}

View File

@ -1,17 +0,0 @@
package me.rhunk.snapenhance.config.impl
import me.rhunk.snapenhance.config.ConfigValue
class ConfigIntegerValue(
private var value: Int
) : ConfigValue<Int>() {
override fun value() = value
override fun read(): String {
return value.toString()
}
override fun write(value: String) {
this.value = value.toInt()
}
}

View File

@ -1,32 +0,0 @@
package me.rhunk.snapenhance.config.impl
import me.rhunk.snapenhance.config.ConfigValue
class ConfigStateListValue(
private val keys: List<String>,
private var states: MutableMap<String, Boolean> = mutableMapOf()
) : ConfigValue<Map<String, Boolean>>() {
override fun value() = states
fun setKey(key: String, state: Boolean) {
states[key] = state
onValueChanged()
}
operator fun get(key: String) = states[key] ?: false
override fun read(): String {
return keys.joinToString("|") { "$it:${states[it]}" }
}
override fun write(value: String) {
value.split("|").forEach {
val (key, state) = it.split(":")
states[key] = state.toBoolean()
}
}
override fun toString(): String {
return states.filter { it.value }.keys.joinToString(", ") { it }
}
}

View File

@ -1,22 +0,0 @@
package me.rhunk.snapenhance.config.impl
import me.rhunk.snapenhance.config.ConfigValue
class ConfigStateSelection(
private val keys: List<String>,
private var state: String = ""
) : ConfigValue<String>() {
fun keys(): List<String> {
return keys
}
override fun value() = state
override fun read(): String {
return state
}
override fun write(value: String) {
state = value
}
}

View File

@ -1,17 +0,0 @@
package me.rhunk.snapenhance.config.impl
import me.rhunk.snapenhance.config.ConfigValue
class ConfigStateValue(
private var value: Boolean
) : ConfigValue<Boolean>() {
override fun value() = value
override fun read(): String {
return value.toString()
}
override fun write(value: String) {
this.value = value.toBoolean()
}
}

View File

@ -1,21 +0,0 @@
package me.rhunk.snapenhance.config.impl
import me.rhunk.snapenhance.config.ConfigValue
class ConfigStringValue(
private var value: String = "",
val isFolderPath: Boolean = false,
val isHidden: Boolean = false
) : ConfigValue<String>() {
override fun value() = value
fun hiddenValue() = if (isHidden) value.map { '*' }.joinToString("") else value
override fun read(): String {
return value
}
override fun write(value: String) {
this.value = value
}
}

View File

@ -1,7 +1,6 @@
package me.rhunk.snapenhance.core.config
import com.google.gson.JsonObject
import me.rhunk.snapenhance.Logger
import kotlin.reflect.KProperty
typealias ConfigParamsBuilder = ConfigParams.() -> Unit
@ -57,7 +56,7 @@ open class ConfigContainer(
fun toJson(): JsonObject {
val json = JsonObject()
properties.forEach { (propertyKey, propertyValue) ->
val serializedValue = propertyValue.getNullable()?.let { propertyKey.dataProcessor.serializeAny(it) }
val serializedValue = propertyValue.getNullable()?.let { propertyKey.dataType.serializeAny(it) }
json.add(propertyKey.name, serializedValue)
}
return json
@ -66,7 +65,7 @@ open class ConfigContainer(
fun fromJson(json: JsonObject) {
properties.forEach { (key, _) ->
val jsonElement = json.get(key.name) ?: return@forEach
key.dataProcessor.deserializeAny(jsonElement)?.let {
key.dataType.deserializeAny(jsonElement)?.let {
properties[key]?.setAny(it)
}
}

View File

@ -1,12 +1,20 @@
package me.rhunk.snapenhance.core.config
import me.rhunk.snapenhance.Logger
import kotlin.reflect.KProperty
data class PropertyPair<T>(
val key: PropertyKey<T>,
val value: PropertyValue<*>
) {
val name get() = key.name
}
class ConfigParams(
var shouldTranslate: Boolean = false,
var hidden: Boolean = false,
var isFolder: Boolean = false
var isFolder: Boolean = false,
val disabledKey: String? = null
)
class PropertyValue<T>(
@ -34,7 +42,7 @@ class PropertyValue<T>(
class PropertyKey<T>(
val name: String,
val dataProcessor: DataProcessors.PropertyDataProcessor<T>,
val dataType: DataProcessors.PropertyDataProcessor<T>,
val params: ConfigParams = ConfigParams(),
)

View File

@ -65,7 +65,7 @@ object DataProcessors {
type = Type.STRING_MULTIPLE_SELECTION,
serialize = { JsonArray().apply { it.forEach { add(it) } } },
deserialize = { obj ->
obj.asJsonArray.map { it.asString }
obj.asJsonArray.map { it.asString }.toMutableList()
},
)

View File

@ -5,7 +5,7 @@ import me.rhunk.snapenhance.data.NotificationType
class Global : ConfigContainer() {
val snapchatPlus = boolean("snapchat_plus")
val autoUpdater = unique("auto_updater", "DAILY","EVERY_LAUNCH", "DAILY", "WEEKLY")
val autoUpdater = unique("auto_updater", "EVERY_LAUNCH", "DAILY", "WEEKLY")
val disableMetrics = boolean("disable_metrics")
val disableVideoLengthRestrictions = boolean("disable_video_length_restrictions")
val disableGooglePlayDialogs = boolean("disable_google_play_dialogs")

View File

@ -1,8 +1,6 @@
package me.rhunk.snapenhance.core.config.impl
import me.rhunk.snapenhance.core.config.ConfigContainer
import me.rhunk.snapenhance.data.NotificationType
import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks
class RootConfig : ConfigContainer() {
val downloader = container("downloader", DownloaderConfig())

View File

@ -18,8 +18,6 @@ import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.bridge.DownloadCallback
import me.rhunk.snapenhance.bridge.wrapper.ConfigWrapper
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.core.config.ModConfig
import me.rhunk.snapenhance.data.FileType
import me.rhunk.snapenhance.download.data.DownloadMetadata

View File

@ -1,7 +1,6 @@
package me.rhunk.snapenhance.features.impl
import android.annotation.SuppressLint
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage
@ -95,10 +94,6 @@ class ConfigEnumKeys : Feature("Config enum keys", loadParams = FeatureLoadParam
}
}
ConfigProperty.ENABLE_APP_APPEARANCE.valueContainer.addPropertyChangeListener {
context.softRestartApp(true)
}
val sharedPreferencesImpl = context.androidContext.classLoader.loadClass("android.app.SharedPreferencesImpl")
sharedPreferencesImpl.methods.first { it.name == "getBoolean" }.hook(HookStage.BEFORE) { param ->

View File

@ -1,14 +1,13 @@
package me.rhunk.snapenhance.features.impl
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.features.impl.spying.StealthMode
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import me.rhunk.snapenhance.hook.hook
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.features.impl.spying.StealthMode;
class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) {
lateinit var conversationManager: Any

View File

@ -11,7 +11,6 @@ import me.rhunk.snapenhance.Constants.ARROYO_URL_KEY_PROTO_PATH
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.Logger.xposedLog
import me.rhunk.snapenhance.bridge.DownloadCallback
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.FileType
import me.rhunk.snapenhance.data.wrapper.impl.media.MediaInfo

View File

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.res.TypedArray
import android.graphics.drawable.ColorDrawable
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage

View File

@ -1,7 +1,6 @@
package me.rhunk.snapenhance.features.impl.experiments
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.os.Build
import android.text.Editable
@ -9,7 +8,6 @@ import android.text.InputType
import android.text.TextWatcher
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.ui.ViewAppearanceHelper

View File

@ -1,7 +1,6 @@
package me.rhunk.snapenhance.features.impl.experiments
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.features.impl.experiments
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.features.impl.experiments
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.features.impl.experiments
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage

View File

@ -1,10 +1,8 @@
package me.rhunk.snapenhance.features.impl.privacy
import de.robv.android.xposed.XposedHelpers
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookAdapter
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker

View File

@ -1,7 +1,6 @@
package me.rhunk.snapenhance.features.impl.privacy
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.NotificationType
import me.rhunk.snapenhance.data.wrapper.impl.MessageContent
import me.rhunk.snapenhance.features.Feature

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.features.impl.spying
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage

View File

@ -4,7 +4,6 @@ import android.os.DeadObjectException
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.MessageState
import me.rhunk.snapenhance.data.wrapper.impl.Message

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.features.impl.spying
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams

View File

@ -1,7 +1,6 @@
package me.rhunk.snapenhance.features.impl.tweaks
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.MessageState
import me.rhunk.snapenhance.data.wrapper.impl.Message
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID

View File

@ -5,7 +5,6 @@ import android.annotation.SuppressLint
import android.content.ContextWrapper
import android.content.pm.PackageManager
import android.hardware.camera2.CameraManager
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.wrapper.impl.ScSize
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.features.impl.tweaks
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage

View File

@ -1,7 +1,5 @@
package me.rhunk.snapenhance.features.impl.tweaks
import android.app.AlertDialog
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.MessageSender
import me.rhunk.snapenhance.data.wrapper.impl.MessageContent

View File

@ -2,7 +2,6 @@ package me.rhunk.snapenhance.features.impl.tweaks
import android.app.AlertDialog
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage

View File

@ -1,7 +1,6 @@
package me.rhunk.snapenhance.features.impl.tweaks
import android.content.Intent
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.features.impl.tweaks
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage

View File

@ -14,7 +14,6 @@ import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.MediaReferenceType
import me.rhunk.snapenhance.data.wrapper.impl.Message

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.features.impl.tweaks
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.features.impl.tweaks
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.MessageState
import me.rhunk.snapenhance.data.wrapper.impl.Message

View File

@ -5,7 +5,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage

View File

@ -8,7 +8,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookAdapter

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.manager.impl
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.ModContext
import me.rhunk.snapenhance.action.AbstractAction
import me.rhunk.snapenhance.action.impl.CheckForUpdates
@ -9,6 +8,7 @@ import me.rhunk.snapenhance.action.impl.ClearMessageLogger
import me.rhunk.snapenhance.action.impl.ExportChatMessages
import me.rhunk.snapenhance.action.impl.OpenMap
import me.rhunk.snapenhance.action.impl.RefreshMappings
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.manager.Manager
import kotlin.reflect.KClass

View File

@ -1,58 +0,0 @@
package me.rhunk.snapenhance.manager.impl
import com.google.gson.JsonObject
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.ModContext
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.config.ConfigAccessor
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.manager.Manager
import java.nio.charset.StandardCharsets
class ConfigManager(
private val context: ModContext
) : ConfigAccessor(), Manager {
override fun init() {
ConfigProperty.sortedByCategory().forEach { key ->
set(key, key.valueContainer)
}
if (!context.bridgeClient.isFileExists(BridgeFileType.CONFIG)) {
writeConfig()
return
}
runCatching {
loadConfig()
}.onFailure {
Logger.xposedLog("Failed to load config", it)
writeConfig()
}
}
private fun loadConfig() {
val configContent = context.bridgeClient.createAndReadFile(
BridgeFileType.CONFIG,
"{}".toByteArray(Charsets.UTF_8)
)
val configObject: JsonObject = context.gson.fromJson(
String(configContent, StandardCharsets.UTF_8),
JsonObject::class.java
)
entries().forEach { (key, value) ->
value.writeFrom(configObject.get(key.name)?.asString ?: value.read())
}
}
fun writeConfig() {
val configObject = JsonObject()
entries().forEach { (key, value) ->
configObject.addProperty(key.name, value.read())
}
context.bridgeClient.writeFile(
BridgeFileType.CONFIG,
context.gson.toJson(configObject).toByteArray(Charsets.UTF_8)
)
}
}

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.manager.impl
import android.app.AlertDialog
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import me.rhunk.snapenhance.Constants

View File

@ -1,87 +0,0 @@
package me.rhunk.snapenhance.ui
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import me.rhunk.snapenhance.core.R
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.util.ActivityResultCallback
import kotlin.math.abs
import kotlin.random.Random
class ItemHelper(
private val context : Context
) {
val positiveButtonText by lazy {
SharedContext.translation["button.ok"]
}
val cancelButtonText by lazy {
SharedContext.translation["button.cancel"]
}
fun longToast(message: String, context: Context) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
fun createTranslatedTextView(property: ConfigProperty, shouldTranslatePropertyValue: Boolean = true): TextView {
return object: TextView(context) {
override fun setText(text: CharSequence?, type: BufferType?) {
val newText = text?.takeIf { it.isNotEmpty() }?.let {
if (!shouldTranslatePropertyValue || property.disableValueLocalization) it
else SharedContext.translation[property.getOptionTranslationKey(it.toString())]
}?.let {
if (it.length > 20) {
"...${it.substring(it.length - 20)}"
} else {
it
}
} ?: ""
super.setTextColor(context.getColor(R.color.tertiaryText))
super.setText(newText, type)
}
}
}
fun askForValue(property: ConfigProperty, requestedInputType: Int, callback: (String) -> Unit) {
val editText = EditText(context).apply {
inputType = requestedInputType
setText(property.valueContainer.value().toString())
}
AlertDialog.Builder(context)
.setTitle(SharedContext.translation["property.${property.translationKey}.name"])
.setView(editText)
.setPositiveButton(positiveButtonText) { _, _ ->
callback(editText.text.toString())
}
.setNegativeButton(cancelButtonText) { dialog, _ ->
dialog.cancel()
}
.show()
}
fun askForFolder(activity: Activity, property: ConfigProperty, callback: (String) -> Unit): Pair<Int, ActivityResultCallback> {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val requestCode = abs(Random.nextInt())
activity.startActivityForResult(intent, requestCode)
return requestCode to let@{_, resultCode, data ->
if (resultCode != Activity.RESULT_OK) return@let
val uri = data?.data ?: return@let
val value = uri.toString()
activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
property.valueContainer.writeFrom(value)
callback(value)
}
}
}

View File

@ -1,279 +0,0 @@
package me.rhunk.snapenhance.ui.config
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle
import android.text.Html
import android.text.InputType
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.Switch
import android.widget.TextView
import android.widget.Toast
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.core.R
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.config.ConfigCategory
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.config.impl.ConfigIntegerValue
import me.rhunk.snapenhance.config.impl.ConfigStateListValue
import me.rhunk.snapenhance.config.impl.ConfigStateSelection
import me.rhunk.snapenhance.config.impl.ConfigStateValue
import me.rhunk.snapenhance.config.impl.ConfigStringValue
import me.rhunk.snapenhance.ui.ItemHelper
import me.rhunk.snapenhance.util.ActivityResultCallback
import kotlin.system.exitProcess
class ConfigActivity : Activity() {
private val itemHelper = ItemHelper(this)
private val activityResultCallbacks = mutableMapOf<Int, ActivityResultCallback>()
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onBackPressed() {
super.onBackPressed()
finish()
}
override fun onDestroy() {
super.onDestroy()
SharedContext.config.writeConfig()
}
override fun onPause() {
super.onPause()
SharedContext.config.writeConfig()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
activityResultCallbacks[requestCode]?.invoke(requestCode, resultCode, data)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SharedContext.ensureInitialized(this)
setContentView(R.layout.config_activity)
findViewById<View>(R.id.title_bar).let { titleBar ->
titleBar.findViewById<TextView>(R.id.title).text = SharedContext.translation["config_activity.title"]
titleBar.findViewById<ImageButton>(R.id.back_button).visibility = View.GONE
}
val propertyListLayout = findViewById<ViewGroup>(R.id.property_list)
if (intent.getBooleanExtra("lspatched", false) ||
applicationInfo.packageName != "me.rhunk.snapenhance" ||
BuildConfig.DEBUG) {
propertyListLayout.addView(
layoutInflater.inflate(
R.layout.config_activity_debug_item,
propertyListLayout,
false
).apply {
findViewById<TextView>(R.id.debug_item_content).apply {
text = Html.fromHtml(
"You are using a <u><b>debug/unofficial</b></u> build!\n" +
"Please consider downloading stable builds from <a href=\"https://github.com/rhunk/SnapEnhance\">GitHub</a>.",
Html.FROM_HTML_MODE_COMPACT
)
movementMethod = android.text.method.LinkMovementMethod.getInstance()
}
})
}
//check if save folder is set
//TODO: first run activity
run {
val saveFolder = SharedContext.config.string(ConfigProperty.SAVE_FOLDER)
val itemHelper = ItemHelper(this)
if (saveFolder.isEmpty() || !saveFolder.startsWith("content://")) {
AlertDialog.Builder(this)
.setTitle("Save folder")
.setMessage("Please select a folder where you want to save downloaded files.")
.setPositiveButton("Select") { _, _ ->
val (requestCode, callback) = itemHelper.askForFolder(
this,
ConfigProperty.SAVE_FOLDER
) {}
activityResultCallbacks[requestCode] = { a1, a2, a3 ->
callback(a1, a2, a3)
Toast.makeText(this, "Save Folder set!", Toast.LENGTH_SHORT).show()
finish()
}
}
.setNegativeButton("Cancel") { _, _ ->
exitProcess(0)
}
.show()
}
}
var currentCategory: ConfigCategory? = null
SharedContext.config.entries().filter { !it.key.category.hidden }.forEach { (property, value) ->
val configItem = layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false)
fun addSeparator() {
//add separator
propertyListLayout.addView(View(this).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)
setBackgroundColor(getColor(R.color.tertiaryBackground))
})
}
if (property.category != currentCategory) {
if(!property.shouldAppearInSettings) return@forEach
currentCategory = property.category
with(layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false)) {
findViewById<TextView>(R.id.name).apply {
text = SharedContext.translation["category.${property.category.key}"]
textSize = 20f
typeface = typeface?.let { android.graphics.Typeface.create(it, android.graphics.Typeface.BOLD) }
}
propertyListLayout.addView(this)
}
addSeparator()
}
if (!property.shouldAppearInSettings) return@forEach
val propertyName = SharedContext.translation["property.${property.translationKey}.name"]
configItem.findViewById<TextView>(R.id.name).text = propertyName
configItem.findViewById<TextView>(R.id.description).also {
it.text = SharedContext.translation["property.${property.translationKey}.description"]
it.visibility = if (it.text.isEmpty()) View.GONE else View.VISIBLE
}
fun addValueView(view: View) {
configItem.findViewById<ViewGroup>(R.id.value).addView(view)
}
when (value) {
is ConfigStateValue -> {
val switch = Switch(this)
switch.isChecked = value.value()
switch.trackTintList = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
),
intArrayOf(
switch.highlightColor,
getColor(R.color.tertiaryBackground)
)
)
switch.setOnCheckedChangeListener { _, isChecked ->
value.writeFrom(isChecked.toString())
}
configItem.setOnClickListener { switch.toggle() }
addValueView(switch)
}
is ConfigStringValue, is ConfigIntegerValue -> {
val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = false).also {
it.text = value.value().toString()
}
configItem.setOnClickListener {
if (value is ConfigStringValue && value.isFolderPath) {
val (requestCode, callback) = itemHelper.askForFolder(this, property) {
value.writeFrom(it)
textView.text = value.value()
}
activityResultCallbacks[requestCode] = callback
return@setOnClickListener
}
if (value is ConfigIntegerValue) {
itemHelper.askForValue(property, InputType.TYPE_CLASS_NUMBER) {
try {
value.writeFrom(it)
textView.text = value.value().toString()
} catch (e: NumberFormatException) {
itemHelper.longToast(SharedContext.translation["config_activity.invalid_number_toast"], this)
}
}
return@setOnClickListener
}
itemHelper.askForValue(property, InputType.TYPE_CLASS_TEXT) {
value.writeFrom(it)
textView.text = value.value().toString()
}
}
addValueView(textView)
}
is ConfigStateListValue -> {
val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = false)
val values = value.value()
fun updateText() {
textView.text = SharedContext.translation.format("config_activity.selected_text", "count" to values.filter { it.value }.size.toString())
}
updateText()
configItem.setOnClickListener {
AlertDialog.Builder(this)
.setTitle(propertyName)
.setPositiveButton(itemHelper.positiveButtonText) { _, _ ->
updateText()
}
.setMultiChoiceItems(
values.keys.map {
if (property.disableValueLocalization) it
else SharedContext.translation[property.getOptionTranslationKey(it)]
}.toTypedArray(),
values.map { it.value }.toBooleanArray()
) { _, which, isChecked ->
value.setKey(values.keys.elementAt(which), isChecked)
}
.show()
}
addValueView(textView)
}
is ConfigStateSelection -> {
val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = true)
textView.text = value.value()
configItem.setOnClickListener {
val builder = AlertDialog.Builder(this)
builder.setTitle(propertyName)
builder.setSingleChoiceItems(
value.keys().toTypedArray().map {
if (property.disableValueLocalization) it
else SharedContext.translation[property.getOptionTranslationKey(it)]
}.toTypedArray(),
value.keys().indexOf(value.value())
) { _, which ->
value.writeFrom(value.keys()[which])
}
builder.setPositiveButton(itemHelper.positiveButtonText) { _, _ ->
textView.text = value.value()
}
builder.show()
}
addValueView(textView)
}
}
propertyListLayout.addView(configItem)
addSeparator()
}
propertyListLayout.addView(layoutInflater.inflate(R.layout.config_activity_debug_item, propertyListLayout, false).apply {
findViewById<TextView>(R.id.debug_item_content).apply {
text = Html.fromHtml("Made by rhunk on <a href=\"https://github.com/rhunk/SnapEnhance\">GitHub</a>", Html.FROM_HTML_MODE_COMPACT)
movementMethod = android.text.method.LinkMovementMethod.getInstance()
}
})
}
}

View File

@ -1,7 +1,6 @@
package me.rhunk.snapenhance.ui.download
import android.app.AlertDialog
import android.content.Intent
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
@ -9,11 +8,9 @@ import android.widget.ImageButton
import android.widget.ListView
import android.widget.TextView
import android.widget.Toast
import me.rhunk.snapenhance.core.R
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.ui.config.ConfigActivity
import me.rhunk.snapenhance.ui.spoof.DeviceSpooferActivity
import me.rhunk.snapenhance.core.R
import java.io.File
class ActionListAdapter(
@ -68,12 +65,6 @@ class DebugSettingsLayoutInflater(
debugSettingsLayout.findViewById<ListView>(R.id.setting_page_list).apply {
adapter = ActionListAdapter(activity, R.layout.debug_setting_item, mutableListOf<Pair<String, () -> Unit>>().apply {
add(SharedContext.translation["config_activity.title"] to {
activity.startActivity(Intent(activity, ConfigActivity::class.java))
})
add(SharedContext.translation["spoof_activity.title"] to {
activity.startActivity(Intent(activity, DeviceSpooferActivity::class.java))
})
add(debugSettingsTranslation["clear_cache_title"] to {
context.cacheDir.listFiles()?.forEach {
it.deleteRecursively()

View File

@ -23,8 +23,8 @@ import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.core.R
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.core.R
import me.rhunk.snapenhance.data.FileType
import me.rhunk.snapenhance.download.data.PendingDownload
import me.rhunk.snapenhance.download.enums.DownloadStage

View File

@ -15,10 +15,10 @@ import android.widget.ImageButton
import android.widget.TextView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.core.R
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.core.R
import me.rhunk.snapenhance.download.data.PendingDownload
class DownloadManagerActivity : Activity() {

View File

@ -10,7 +10,6 @@ import android.widget.Button
import android.widget.LinearLayout
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.Constants.VIEW_INJECTED_CODE
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.features.impl.spying.MessageLogger

View File

@ -21,7 +21,6 @@ import android.widget.LinearLayout
import android.widget.Switch
import android.widget.Toast
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.wrapper.impl.FriendActionButton
import me.rhunk.snapenhance.database.objects.ConversationMessage

View File

@ -6,9 +6,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import de.robv.android.xposed.XC_MethodHook.Unhook
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.features.impl.Messaging

View File

@ -10,8 +10,8 @@ import android.widget.ScrollView
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.ui.menu.AbstractMenu
import me.rhunk.snapenhance.ui.ViewAppearanceHelper.applyTheme
import me.rhunk.snapenhance.ui.menu.AbstractMenu
@SuppressLint("DiscouragedApi")
class OperaContextActionMenu : AbstractMenu() {

View File

@ -6,9 +6,8 @@ import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.ui.config.ConfigActivity
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.ui.menu.AbstractMenu
import java.io.File

View File

@ -1,26 +1,13 @@
package me.rhunk.snapenhance.ui.menu.impl
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.graphics.Color
import android.graphics.Typeface
import android.text.InputType
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Switch
import android.widget.TextView
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.config.impl.ConfigIntegerValue
import me.rhunk.snapenhance.config.impl.ConfigStateListValue
import me.rhunk.snapenhance.config.impl.ConfigStateSelection
import me.rhunk.snapenhance.config.impl.ConfigStateValue
import me.rhunk.snapenhance.config.impl.ConfigStringValue
import me.rhunk.snapenhance.ui.menu.AbstractMenu
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.ui.menu.AbstractMenu
class SettingsMenu : AbstractMenu() {
/*
@SuppressLint("ClickableViewAccessibility")
private fun createCategoryTitle(key: String): TextView {
val categoryText = TextView(context.androidContext)
@ -186,7 +173,7 @@ class SettingsMenu : AbstractMenu() {
setPadding(0, 0, 0, thickness)
setBackgroundColor(color)
}
}
}*/
//TODO: quick settings
@SuppressLint("SetTextI18n")
@ -194,9 +181,7 @@ class SettingsMenu : AbstractMenu() {
val actions = context.actionManager.getActions().map {
Pair(it) {
val button = Button(viewModel.context)
button.text = (it.dependsOnProperty?.let { property ->
"["+context.translation["property.${property.translationKey}.name"] + "] "
}?: "") + context.translation[it.nameKey]
button.text = context.translation[it.nameKey]
button.setOnClickListener { _ ->
it.run()

View File

@ -1,111 +0,0 @@
package me.rhunk.snapenhance.ui.spoof
import android.app.Activity
import android.content.res.ColorStateList
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.Switch
import android.widget.TextView
import me.rhunk.snapenhance.core.R
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.config.ConfigCategory
import me.rhunk.snapenhance.config.impl.ConfigIntegerValue
import me.rhunk.snapenhance.config.impl.ConfigStateValue
import me.rhunk.snapenhance.config.impl.ConfigStringValue
import me.rhunk.snapenhance.ui.ItemHelper
class DeviceSpooferActivity: Activity() {
private val itemHelper = ItemHelper(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SharedContext.ensureInitialized(this)
setContentView(R.layout.device_spoofer_activity)
findViewById<TextView>(R.id.title).text = "Device Spoofer"
findViewById<ImageButton>(R.id.back_button).setOnClickListener { finish() }
val propertyListLayout = findViewById<ViewGroup>(R.id.spoof_property_list)
SharedContext.config.entries().filter { it.key.category == ConfigCategory.DEVICE_SPOOFER }.forEach { (property, value) ->
val configItem = layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false)
val propertyName = SharedContext.translation["property.${property.translationKey}.name"]
fun addSeparator() {
//add separator
propertyListLayout.addView(View(this).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)
setBackgroundColor(getColor(R.color.tertiaryBackground))
})
}
configItem.findViewById<TextView>(R.id.name).text = propertyName
configItem.findViewById<TextView>(R.id.description).also {
it.text = SharedContext.translation["property.${property.translationKey}.description"]
it.visibility = if (it.text.isEmpty()) View.GONE else View.VISIBLE
}
fun addValueView(view: View) {
configItem.findViewById<ViewGroup>(R.id.value).addView(view)
}
when (value) {
is ConfigStateValue -> {
val switch = Switch(this)
switch.isChecked = value.value()
switch.trackTintList = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
),
intArrayOf(
switch.highlightColor,
getColor(R.color.tertiaryBackground)
)
)
switch.setOnCheckedChangeListener { _, isChecked ->
value.writeFrom(isChecked.toString())
}
configItem.setOnClickListener { switch.toggle() }
addValueView(switch)
}
is ConfigStringValue, is ConfigIntegerValue -> {
val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = false).also {
it.text = value.value().toString()
}
configItem.setOnClickListener {
if (value is ConfigIntegerValue) {
itemHelper.askForValue(property, InputType.TYPE_CLASS_NUMBER) {
try {
value.writeFrom(it)
textView.text = value.value().toString()
} catch (e: NumberFormatException) {
itemHelper.longToast(SharedContext.translation["config_activity.invalid_number_toast"], this)
}
}
return@setOnClickListener
}
itemHelper.askForValue(property, InputType.TYPE_CLASS_TEXT) {
value.writeFrom(it)
textView.text = value.value().toString()
}
}
addValueView(textView)
}
}
propertyListLayout.addView(configItem)
addSeparator()
}
}
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onBackPressed() {
super.onBackPressed()
finish()
}
}

View File

@ -10,9 +10,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.ModContext
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.FileType
import me.rhunk.snapenhance.data.MediaReferenceType

View File

@ -1,10 +1,10 @@
[versions]
agp = "8.2.0-alpha13"
agp = "8.2.0-alpha14"
androidx-material = "1.6.0-alpha02"
junit = "4.13.2"
kotlin = "1.8.22"
kotlinx-coroutines-android = "1.7.2"
kotlin-reflect = "1.8.21"
kotlin-reflect = "1.8.22"
material-icons-extended = "1.6.0-alpha03"
navigation-compose = "2.6.0"
recyclerview = "1.3.1"