ui: home screen
- monochrome launcher icon
@ -85,6 +85,10 @@ dependencies {
|
|||||||
implementation(libs.androidx.activity.ktx)
|
implementation(libs.androidx.activity.ktx)
|
||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.androidx.material)
|
implementation(libs.androidx.material)
|
||||||
|
|
||||||
|
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
|
||||||
|
implementation("androidx.compose.ui:ui-tooling-preview:1.4.3")
|
||||||
|
implementation(kotlin("reflect"))
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
|
@ -7,16 +7,16 @@
|
|||||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<queries>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<package android:name="com.snapchat.android" />
|
||||||
tools:ignore="ScopedStorage" />
|
</queries>
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
|
||||||
tools:ignore="ScopedStorage" />
|
|
||||||
<application
|
<application
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
tools:targetApi="31"
|
tools:targetApi="34"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:icon="@mipmap/launcher_icon">
|
android:icon="@mipmap/launcher_icon">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="xposedmodule"
|
android:name="xposedmodule"
|
||||||
|
@ -16,10 +16,12 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.navigation
|
import androidx.navigation.compose.navigation
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import me.rhunk.snapenhance.manager.data.ManagerContext
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
|
||||||
@ -27,39 +29,23 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
App()
|
App(ManagerContext(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App() {
|
fun App(
|
||||||
|
context: ManagerContext
|
||||||
|
) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
val navigation = Navigation(context)
|
||||||
AppMaterialTheme {
|
AppMaterialTheme {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(text = "SnapEnhance") },
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = { /*TODO*/ }) {
|
|
||||||
Icon(Icons.Filled.Settings, contentDescription = "Settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
bottomBar = { NavBar(navController = navController) }
|
bottomBar = { navigation.NavBar(navController = navController) }
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
NavHost(navController, startDestination = "main", Modifier.padding(innerPadding)) {
|
navigation.NavigationHost(navController = navController, innerPadding = innerPadding)
|
||||||
navigation(MainSections.HOME.route, "main") {
|
|
||||||
MainSections.values().toList().forEach { section ->
|
|
||||||
composable(section.route) {
|
|
||||||
section.content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,113 +1,100 @@
|
|||||||
package me.rhunk.snapenhance.manager
|
package me.rhunk.snapenhance.manager
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.requiredWidth
|
import androidx.compose.foundation.layout.requiredWidth
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.BugReport
|
|
||||||
import androidx.compose.material.icons.filled.Download
|
|
||||||
import androidx.compose.material.icons.filled.Group
|
|
||||||
import androidx.compose.material.icons.filled.Home
|
|
||||||
import androidx.compose.material.icons.filled.Stars
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
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 androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import me.rhunk.snapenhance.manager.sections.NotImplemented
|
import me.rhunk.snapenhance.manager.data.ManagerContext
|
||||||
|
|
||||||
|
|
||||||
enum class MainSections(
|
class Navigation(
|
||||||
val route: String,
|
private val context: ManagerContext
|
||||||
val title: String,
|
|
||||||
val icon: ImageVector,
|
|
||||||
val content: @Composable () -> Unit
|
|
||||||
) {
|
) {
|
||||||
DOWNLOADS(
|
@Composable
|
||||||
route = "downloads",
|
fun NavigationHost(
|
||||||
title = "Downloads",
|
navController: NavHostController,
|
||||||
icon = Icons.Filled.Download,
|
innerPadding: PaddingValues
|
||||||
content = { NotImplemented() }
|
) {
|
||||||
),
|
val sections = remember { EnumSection.values().toList().map {
|
||||||
FEATURES(
|
it to it.section.constructors.first().call()
|
||||||
route = "features",
|
}.onEach { (_, instance) ->
|
||||||
title = "Features",
|
instance.manager = context
|
||||||
icon = Icons.Filled.Stars,
|
instance.navController = navController
|
||||||
content = { NotImplemented() }
|
} }
|
||||||
),
|
val homeSection = EnumSection.HOME
|
||||||
HOME(
|
|
||||||
route = "home",
|
|
||||||
title = "Home",
|
|
||||||
icon = Icons.Filled.Home,
|
|
||||||
content = { NotImplemented() }
|
|
||||||
),
|
|
||||||
FRIENDS(
|
|
||||||
route = "friends",
|
|
||||||
title = "Friends",
|
|
||||||
icon = Icons.Filled.Group,
|
|
||||||
content = { NotImplemented() }
|
|
||||||
),
|
|
||||||
DEBUG(
|
|
||||||
route = "debug",
|
|
||||||
title = "Debug",
|
|
||||||
icon = Icons.Filled.BugReport,
|
|
||||||
content = { NotImplemented() }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
NavHost(navController, startDestination = homeSection.route, Modifier.padding(innerPadding)) {
|
||||||
fun NavBar(
|
sections.forEach { (section, instance) ->
|
||||||
navController: NavController
|
composable(section.route) {
|
||||||
) {
|
instance.Content()
|
||||||
NavigationBar {
|
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
|
||||||
val currentDestination = navBackStackEntry?.destination
|
|
||||||
MainSections.values().toList().forEach { section ->
|
|
||||||
fun selected() = currentDestination?.hierarchy?.any { it.route == section.route } == true
|
|
||||||
|
|
||||||
NavigationBarItem(
|
|
||||||
modifier = Modifier
|
|
||||||
.requiredWidth(120.dp)
|
|
||||||
.fillMaxHeight(),
|
|
||||||
icon = {
|
|
||||||
val iconOffset by animateDpAsState(
|
|
||||||
if (selected()) 0.dp else 10.dp,
|
|
||||||
label = ""
|
|
||||||
)
|
|
||||||
Icon(
|
|
||||||
imageVector = section.icon,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.offset(y = iconOffset)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
label = {
|
|
||||||
val labelOffset by animateDpAsState(
|
|
||||||
if (selected()) 0.dp else (-5).dp,
|
|
||||||
label = ""
|
|
||||||
)
|
|
||||||
Text(text = if (selected()) section.title else "", modifier = Modifier.offset(y = labelOffset))
|
|
||||||
},
|
|
||||||
selected = selected(),
|
|
||||||
onClick = {
|
|
||||||
navController.navigate(section.route) {
|
|
||||||
popUpTo(navController.graph.findStartDestination().id) {
|
|
||||||
saveState = true
|
|
||||||
}
|
|
||||||
launchSingleTop = true
|
|
||||||
restoreState = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Composable
|
||||||
|
fun NavBar(
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
NavigationBar {
|
||||||
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
|
val currentDestination = navBackStackEntry?.destination
|
||||||
|
EnumSection.values().toList().forEach { section ->
|
||||||
|
fun selected() = currentDestination?.hierarchy?.any { it.route == section.route } == true
|
||||||
|
|
||||||
|
NavigationBarItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredWidth(120.dp)
|
||||||
|
.fillMaxHeight(),
|
||||||
|
icon = {
|
||||||
|
val iconOffset by animateDpAsState(
|
||||||
|
if (selected()) 0.dp else 10.dp,
|
||||||
|
label = ""
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = section.icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.offset(y = iconOffset)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
label = {
|
||||||
|
val labelOffset by animateDpAsState(
|
||||||
|
if (selected()) 0.dp else (-5).dp,
|
||||||
|
label = ""
|
||||||
|
)
|
||||||
|
Text(text = if (selected()) section.title else "", modifier = Modifier.offset(y = labelOffset))
|
||||||
|
},
|
||||||
|
selected = selected(),
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(section.route) {
|
||||||
|
popUpTo(navController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
61
app/src/main/kotlin/me/rhunk/snapenhance/manager/Section.kt
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package me.rhunk.snapenhance.manager
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.BugReport
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.Group
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.filled.Stars
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
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 kotlin.reflect.KClass
|
||||||
|
|
||||||
|
enum class EnumSection(
|
||||||
|
val route: String,
|
||||||
|
val title: String,
|
||||||
|
val icon: ImageVector,
|
||||||
|
val section: KClass<out Section> = NotImplemented::class
|
||||||
|
) {
|
||||||
|
DOWNLOADS(
|
||||||
|
route = "downloads",
|
||||||
|
title = "Downloads",
|
||||||
|
icon = Icons.Filled.Download
|
||||||
|
),
|
||||||
|
FEATURES(
|
||||||
|
route = "features",
|
||||||
|
title = "Features",
|
||||||
|
icon = Icons.Filled.Stars,
|
||||||
|
section = FeaturesSection::class
|
||||||
|
),
|
||||||
|
HOME(
|
||||||
|
route = "home",
|
||||||
|
title = "Home",
|
||||||
|
icon = Icons.Filled.Home,
|
||||||
|
section = HomeSection::class
|
||||||
|
),
|
||||||
|
FRIENDS(
|
||||||
|
route = "friends",
|
||||||
|
title = "Friends",
|
||||||
|
icon = Icons.Filled.Group
|
||||||
|
),
|
||||||
|
DEBUG(
|
||||||
|
route = "debug",
|
||||||
|
title = "Debug",
|
||||||
|
icon = Icons.Filled.BugReport
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
open class Section {
|
||||||
|
lateinit var manager: ManagerContext
|
||||||
|
lateinit var navController: NavController
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
open fun Content() { NotImplemented() }
|
||||||
|
}
|
@ -74,8 +74,6 @@ val md_theme_dark_scrim = Color(0xFF000000)
|
|||||||
val seed = Color(0xFF6750A4)
|
val seed = Color(0xFF6750A4)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private val LightThemeColors = lightColorScheme(
|
private val LightThemeColors = lightColorScheme(
|
||||||
primary = md_theme_light_primary,
|
primary = md_theme_light_primary,
|
||||||
onPrimary = md_theme_light_onPrimary,
|
onPrimary = md_theme_light_onPrimary,
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
package me.rhunk.snapenhance.manager.data
|
||||||
|
|
||||||
|
|
||||||
|
data class SnapchatAppInfo(
|
||||||
|
val version: String,
|
||||||
|
val versionCode: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ModMappingsInfo(
|
||||||
|
val generatedSnapchatVersion: Long,
|
||||||
|
val isOutdated: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class InstallationSummary(
|
||||||
|
val snapchatInfo: SnapchatAppInfo?,
|
||||||
|
val mappingsInfo: ModMappingsInfo?
|
||||||
|
)
|
@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
class ManagerContext(
|
||||||
|
private val context: Context
|
||||||
|
) {
|
||||||
|
private val config = ConfigWrapper()
|
||||||
|
private val translation = TranslationWrapper()
|
||||||
|
private val mappings = MappingsWrapper(context)
|
||||||
|
|
||||||
|
init {
|
||||||
|
config.loadFromContext(context)
|
||||||
|
translation.loadFromContext(context)
|
||||||
|
mappings.apply { loadFromContext(context) }.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInstallationSummary() = InstallationSummary(
|
||||||
|
snapchatInfo = mappings.getSnapchatPackageInfo()?.let {
|
||||||
|
SnapchatAppInfo(
|
||||||
|
version = it.versionName,
|
||||||
|
versionCode = it.longVersionCode
|
||||||
|
)
|
||||||
|
},
|
||||||
|
mappingsInfo = if (mappings.isMappingsLoaded()) {
|
||||||
|
ModMappingsInfo(
|
||||||
|
generatedSnapchatVersion = mappings.getGeneratedBuildNumber(),
|
||||||
|
isOutdated = mappings.isMappingsOutdated()
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package me.rhunk.snapenhance.manager.sections
|
||||||
|
|
||||||
|
class DebugSection {
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package me.rhunk.snapenhance.manager.sections
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import me.rhunk.snapenhance.manager.Section
|
||||||
|
|
||||||
|
class FeaturesSection : Section() {
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
override fun Content() {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "Features",
|
||||||
|
modifier = Modifier.padding(all = 15.dp),
|
||||||
|
fontSize = 20.sp
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
items(100) { index ->
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(all = 10.dp)
|
||||||
|
.height(70.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(text = "Feature $index", modifier = Modifier.padding(all = 15.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
package me.rhunk.snapenhance.manager.sections
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Map
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import me.rhunk.snapenhance.manager.Section
|
||||||
|
import me.rhunk.snapenhance.manager.data.InstallationSummary
|
||||||
|
|
||||||
|
class HomeSection : Section() {
|
||||||
|
companion object {
|
||||||
|
val cardMargin = 10.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun SummaryCards(installationSummary: InstallationSummary) {
|
||||||
|
//installation summary
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = cardMargin)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(all = 16.dp)) {
|
||||||
|
if (installationSummary.snapchatInfo != null) {
|
||||||
|
Text("Snapchat version: ${installationSummary.snapchatInfo.version}")
|
||||||
|
Text("Snapchat version code: ${installationSummary.snapchatInfo.versionCode}")
|
||||||
|
} else {
|
||||||
|
Text("Snapchat not installed/detected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = cardMargin)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(all = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Map,
|
||||||
|
contentDescription = "Mappings",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 10.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(text = if (installationSummary.mappingsInfo == null || installationSummary.mappingsInfo.isOutdated) {
|
||||||
|
"Mappings ${if (installationSummary.mappingsInfo == null) "not generated" else "outdated"}"
|
||||||
|
} else {
|
||||||
|
"Mappings version ${installationSummary.mappingsInfo.generatedSnapchatVersion}"
|
||||||
|
}, modifier = Modifier.weight(1f)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
|
||||||
|
//inline button
|
||||||
|
Button(onClick = {}, modifier = Modifier.height(40.dp)) {
|
||||||
|
Icon(Icons.Filled.Refresh, contentDescription = "Refresh")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
override fun Content() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(ScrollState(0))
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"SnapEnhance",
|
||||||
|
fontSize = 32.sp,
|
||||||
|
modifier = Modifier.padding(32.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod, nisl eget ultricies ultrices, nunc nisl aliquam nunc, quis aliquam nisl nunc eu nisl. Donec euismod, nisl eget ultricies ultrices, nunc nisl aliquam nunc, quis aliquam nisl nunc eu nisl.",
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
SummaryCards(manager.getInstallationSummary())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,20 @@
|
|||||||
package me.rhunk.snapenhance.manager.sections
|
package me.rhunk.snapenhance.manager.sections
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import me.rhunk.snapenhance.manager.Section
|
||||||
|
|
||||||
@Composable
|
class NotImplemented : Section() {
|
||||||
fun NotImplemented() {
|
@Composable
|
||||||
Column(
|
override fun Content() {
|
||||||
modifier = Modifier.fillMaxSize(),
|
Column {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
Text(text = "Not implemented yet!")
|
||||||
) {
|
}
|
||||||
Text(text = "Not Implemented")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
26
app/src/main/res/drawable/launcher_icon_monochrome.xml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="1000"
|
||||||
|
android:viewportHeight="1000">
|
||||||
|
|
||||||
|
<group
|
||||||
|
android:scaleX="0.65"
|
||||||
|
android:scaleY="0.65"
|
||||||
|
android:translateX="175"
|
||||||
|
android:translateY="175">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffffff"
|
||||||
|
android:pathData="m397.9,491.5h-55.1c-10.1,0 -18.4,8.2 -18.4,18.4h0c0,10.1 8.2,18.4 18.4,18.4h55.1c15.3,0 27.8,13.7 27.8,30.6s-12.5,30.6 -27.8,30.6h-55.1c-10.1,0 -18.4,8.2 -18.4,18.4h0c0,10.1 8.2,18.4 18.4,18.4h55.1c33.8,0 61.2,-30.1 61.2,-67.3s-27.4,-67.3 -61.2,-67.3Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffffff"
|
||||||
|
android:pathData="m814.2,491.5h-62.6v-49.7h-0.1c0,-2 0.1,-4.1 0.1,-6.1 0,-152.1 -123.3,-275.5 -275.5,-275.5s-275.5,123.3 -275.5,275.5c0,2 0,4.1 0.1,6.1h-0.1v302.1c0,29.6 13.5,57.6 36.7,76l11.9,9.4c18.3,13.9 47,13.8 65.2,0l18.5,-14c7.1,-5.4 21,-5.4 28.1,0l18.5,14c18.3,13.9 46.9,13.9 65.2,0l18.5,-14c3.3,-2.5 8.2,-3.8 13.1,-4 4.9,0.2 9.7,1.5 13,4l18.5,14c18.3,13.8 46.8,13.8 65.1,0l18.5,-14c7.1,-5.4 20.9,-5.4 28,0l18.5,14c18.2,13.8 46.8,13.8 65.1,0l11.9,-9.4c23.2,-18.4 36.7,-46.4 36.7,-75.9v-117.8h62.6c33.8,0 61.2,-30.1 61.2,-67.3s-27.4,-67.3 -61.2,-67.3ZM714.8,441.9v310.2c0,16.4 -7.7,31.9 -20.7,41.8l-9.6,7.3c-7.1,5.4 -20.9,5.4 -28,0l-18.5,-14c-18.2,-13.8 -46.8,-13.8 -65.1,0l-18.5,14c-7.1,5.4 -20.9,5.4 -28,0l-18.5,-14c-8.8,-6.7 -20.1,-10.1 -31.4,-10.3v-0c-0.1,0 -0.1,0 -0.2,0 -0,0 -0.1,0 -0.1,0h0c-11.4,0.2 -22.6,3.7 -31.5,10.4l-18.5,14c-7.1,5.4 -21,5.4 -28.1,0l-18.5,-14c-18.3,-13.8 -46.9,-13.8 -65.2,0l-18.5,14c-7.1,5.4 -21,5.4 -28,0l-9.7,-7.4c-13.1,-9.9 -20.8,-25.4 -20.8,-41.8v-310.1h0.1c-0.1,-2 -0.1,-4.1 -0.1,-6.1 0,-131.9 106.9,-238.7 238.7,-238.7s238.7,106.9 238.7,238.7c0,2 -0,4.1 -0.1,6.1h0.1ZM814.2,589.5h-62.6v-61.2h62.6c15.3,0 27.8,13.7 27.8,30.6s-12.5,30.6 -27.8,30.6Z"
|
||||||
|
tools:ignore="VectorPath" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffffff"
|
||||||
|
android:pathData="m711.1,336.1c-6.6,-1.6 -10.6,-1.7 -17.4,-2.8 -33.1,-5.4 -65.4,-0.8 -97.3,8.2 -7,2 -13.9,3.4 -20.9,3.5 -7,-0.2 -13.9,-1.5 -20.9,-3.5 -31.9,-9 -64.2,-13.6 -97.3,-8.2 -6.8,1.1 -10.8,1.1 -17.4,2.8 -6,1.5 -7.5,11.2 -5.5,29.1 0.9,8.2 3.3,16.1 4.9,24.2 1.6,8.1 3.6,16.1 7.9,23.3 4.1,6.8 10.1,11.6 17.5,13.9 16.9,5.4 34.1,6.2 51.3,1.4 14.5,-4.1 26.1,-12.6 33.4,-25.9 5.4,-9.9 9.7,-20.4 14.5,-30.7 0.7,-1.6 1.3,-3.2 2.1,-4.8 2.2,-4.4 5,-6.3 9.6,-6.3 4.5,-0 7.3,1.8 9.6,6.3 0.8,1.6 1.4,3.2 2.1,4.8 4.8,10.3 9.1,20.8 14.5,30.7 7.2,13.4 18.9,21.9 33.4,25.9 17.1,4.8 34.4,4 51.3,-1.4 7.4,-2.3 13.4,-7.1 17.5,-13.9 4.3,-7.2 6.3,-15.1 7.9,-23.3 1.5,-8.1 4,-16 4.9,-24.2 2,-17.9 0.5,-27.6 -5.5,-29.1Z"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
</vector>
|
@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/launcher_icon_background"/>
|
<background android:drawable="@color/launcher_icon_background"/>
|
||||||
<foreground android:drawable="@mipmap/launcher_icon_foreground"/>
|
<foreground android:drawable="@mipmap/launcher_icon_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/launcher_icon_monochrome"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@ -91,7 +91,6 @@ class BridgeClient(
|
|||||||
|
|
||||||
fun deleteFile(fileType: BridgeFileType) = service.deleteFile(fileType.value)
|
fun deleteFile(fileType: BridgeFileType) = service.deleteFile(fileType.value)
|
||||||
|
|
||||||
|
|
||||||
fun isFileExists(fileType: BridgeFileType) = service.isFileExists(fileType.value)
|
fun isFileExists(fileType: BridgeFileType) = service.isFileExists(fileType.value)
|
||||||
|
|
||||||
fun getLoggedMessageIds(conversationId: String, limit: Int): LongArray = service.getLoggedMessageIds(conversationId, limit)
|
fun getLoggedMessageIds(conversationId: String, limit: Int): LongArray = service.getLoggedMessageIds(conversationId, limit)
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
package me.rhunk.snapenhance.bridge
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import me.rhunk.snapenhance.bridge.types.BridgeFileType
|
||||||
|
|
||||||
|
open class FileLoaderWrapper(
|
||||||
|
private val fileType: BridgeFileType,
|
||||||
|
private val defaultContent: ByteArray
|
||||||
|
) {
|
||||||
|
lateinit var isFileExists: () -> Boolean
|
||||||
|
lateinit var write: (ByteArray) -> Unit
|
||||||
|
lateinit var read: () -> ByteArray
|
||||||
|
lateinit var delete: () -> Unit
|
||||||
|
|
||||||
|
fun loadFromContext(context: Context) {
|
||||||
|
val file = fileType.resolve(context)
|
||||||
|
isFileExists = { file.exists() }
|
||||||
|
read = {
|
||||||
|
if (!file.exists()) {
|
||||||
|
file.createNewFile()
|
||||||
|
file.writeBytes("{}".toByteArray(Charsets.UTF_8))
|
||||||
|
}
|
||||||
|
file.readBytes()
|
||||||
|
}
|
||||||
|
write = { file.writeBytes(it) }
|
||||||
|
delete = { file.delete() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadFromBridge(bridgeClient: BridgeClient) {
|
||||||
|
isFileExists = { bridgeClient.isFileExists(fileType) }
|
||||||
|
read = { bridgeClient.createAndReadFile(fileType, defaultContent) }
|
||||||
|
write = { bridgeClient.writeFile(fileType, it) }
|
||||||
|
delete = { bridgeClient.deleteFile(fileType) }
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import com.google.gson.GsonBuilder
|
|||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import me.rhunk.snapenhance.Logger
|
import me.rhunk.snapenhance.Logger
|
||||||
import me.rhunk.snapenhance.bridge.BridgeClient
|
import me.rhunk.snapenhance.bridge.BridgeClient
|
||||||
|
import me.rhunk.snapenhance.bridge.FileLoaderWrapper
|
||||||
import me.rhunk.snapenhance.bridge.types.BridgeFileType
|
import me.rhunk.snapenhance.bridge.types.BridgeFileType
|
||||||
import me.rhunk.snapenhance.config.ConfigAccessor
|
import me.rhunk.snapenhance.config.ConfigAccessor
|
||||||
import me.rhunk.snapenhance.config.ConfigProperty
|
import me.rhunk.snapenhance.config.ConfigProperty
|
||||||
@ -14,16 +15,14 @@ class ConfigWrapper: ConfigAccessor() {
|
|||||||
private val gson = GsonBuilder().setPrettyPrinting().create()
|
private val gson = GsonBuilder().setPrettyPrinting().create()
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var isFileExistsAction: () -> Boolean
|
private val file = FileLoaderWrapper(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8))
|
||||||
private lateinit var writeFileAction: (ByteArray) -> Unit
|
|
||||||
private lateinit var readFileAction: () -> ByteArray
|
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
ConfigProperty.sortedByCategory().forEach { key ->
|
ConfigProperty.sortedByCategory().forEach { key ->
|
||||||
set(key, key.valueContainer)
|
set(key, key.valueContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFileExistsAction()) {
|
if (!file.isFileExists()) {
|
||||||
writeConfig()
|
writeConfig()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -37,7 +36,7 @@ class ConfigWrapper: ConfigAccessor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadConfig() {
|
private fun loadConfig() {
|
||||||
val configContent = readFileAction()
|
val configContent = file.read()
|
||||||
|
|
||||||
val configObject: JsonObject = gson.fromJson(
|
val configObject: JsonObject = gson.fromJson(
|
||||||
configContent.toString(Charsets.UTF_8),
|
configContent.toString(Charsets.UTF_8),
|
||||||
@ -54,27 +53,17 @@ class ConfigWrapper: ConfigAccessor() {
|
|||||||
entries().forEach { (key, value) ->
|
entries().forEach { (key, value) ->
|
||||||
configObject.addProperty(key.name, value.read())
|
configObject.addProperty(key.name, value.read())
|
||||||
}
|
}
|
||||||
writeFileAction(gson.toJson(configObject).toByteArray(Charsets.UTF_8))
|
|
||||||
|
file.write(gson.toJson(configObject).toByteArray(Charsets.UTF_8))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFromContext(context: Context) {
|
fun loadFromContext(context: Context) {
|
||||||
val configFile = BridgeFileType.CONFIG.resolve(context)
|
file.loadFromContext(context)
|
||||||
isFileExistsAction = { configFile.exists() }
|
|
||||||
readFileAction = {
|
|
||||||
if (!configFile.exists()) {
|
|
||||||
configFile.createNewFile()
|
|
||||||
configFile.writeBytes("{}".toByteArray(Charsets.UTF_8))
|
|
||||||
}
|
|
||||||
configFile.readBytes()
|
|
||||||
}
|
|
||||||
writeFileAction = { configFile.writeBytes(it) }
|
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFromBridge(bridgeClient: BridgeClient) {
|
fun loadFromBridge(bridgeClient: BridgeClient) {
|
||||||
isFileExistsAction = { bridgeClient.isFileExists(BridgeFileType.CONFIG) }
|
file.loadFromBridge(bridgeClient)
|
||||||
readFileAction = { bridgeClient.createAndReadFile(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8)) }
|
|
||||||
writeFileAction = { bridgeClient.writeFile(BridgeFileType.CONFIG, it) }
|
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,157 @@
|
|||||||
|
package me.rhunk.snapenhance.bridge.wrapper
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import me.rhunk.snapenhance.Constants
|
||||||
|
import me.rhunk.snapenhance.Logger
|
||||||
|
import me.rhunk.snapenhance.bridge.FileLoaderWrapper
|
||||||
|
import me.rhunk.snapenhance.bridge.types.BridgeFileType
|
||||||
|
import me.rhunk.snapmapper.Mapper
|
||||||
|
import me.rhunk.snapmapper.impl.BCryptClassMapper
|
||||||
|
import me.rhunk.snapmapper.impl.CallbackMapper
|
||||||
|
import me.rhunk.snapmapper.impl.DefaultMediaItemMapper
|
||||||
|
import me.rhunk.snapmapper.impl.EnumMapper
|
||||||
|
import me.rhunk.snapmapper.impl.FriendsFeedEventDispatcherMapper
|
||||||
|
import me.rhunk.snapmapper.impl.MediaQualityLevelProviderMapper
|
||||||
|
import me.rhunk.snapmapper.impl.OperaPageViewControllerMapper
|
||||||
|
import me.rhunk.snapmapper.impl.PlatformAnalyticsCreatorMapper
|
||||||
|
import me.rhunk.snapmapper.impl.PlusSubscriptionMapper
|
||||||
|
import me.rhunk.snapmapper.impl.ScCameraSettingsMapper
|
||||||
|
import me.rhunk.snapmapper.impl.StoryBoostStateMapper
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
|
class MappingsWrapper(
|
||||||
|
private val context: Context,
|
||||||
|
) : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteArray(Charsets.UTF_8)) {
|
||||||
|
companion object {
|
||||||
|
private val gson = GsonBuilder().setPrettyPrinting().create()
|
||||||
|
private val mappers = arrayOf(
|
||||||
|
BCryptClassMapper::class,
|
||||||
|
CallbackMapper::class,
|
||||||
|
DefaultMediaItemMapper::class,
|
||||||
|
MediaQualityLevelProviderMapper::class,
|
||||||
|
EnumMapper::class,
|
||||||
|
OperaPageViewControllerMapper::class,
|
||||||
|
PlatformAnalyticsCreatorMapper::class,
|
||||||
|
PlusSubscriptionMapper::class,
|
||||||
|
ScCameraSettingsMapper::class,
|
||||||
|
StoryBoostStateMapper::class,
|
||||||
|
FriendsFeedEventDispatcherMapper::class
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mappings = ConcurrentHashMap<String, Any>()
|
||||||
|
private var snapBuildNumber: Long = 0
|
||||||
|
|
||||||
|
@Suppress("deprecation")
|
||||||
|
fun init() {
|
||||||
|
snapBuildNumber = getSnapchatVersionCode()
|
||||||
|
|
||||||
|
if (isFileExists()) {
|
||||||
|
runCatching {
|
||||||
|
loadCached()
|
||||||
|
}.onFailure {
|
||||||
|
delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSnapchatPackageInfo() = runCatching {
|
||||||
|
context.packageManager.getPackageInfo(
|
||||||
|
Constants.SNAPCHAT_PACKAGE_NAME,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
fun getSnapchatVersionCode() = getSnapchatPackageInfo()?.longVersionCode ?: -1
|
||||||
|
fun getApplicationSourceDir() = getSnapchatPackageInfo()?.applicationInfo?.sourceDir
|
||||||
|
fun getGeneratedBuildNumber() = snapBuildNumber
|
||||||
|
|
||||||
|
fun isMappingsOutdated(): Boolean {
|
||||||
|
return snapBuildNumber != getSnapchatVersionCode() || isMappingsLoaded().not()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isMappingsLoaded(): Boolean {
|
||||||
|
return mappings.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadCached() {
|
||||||
|
if (!isFileExists()) {
|
||||||
|
throw Exception("Mappings file does not exist")
|
||||||
|
}
|
||||||
|
val mappingsObject = JsonParser.parseString(read().toString(Charsets.UTF_8)).asJsonObject.also {
|
||||||
|
snapBuildNumber = it["snap_build_number"].asLong
|
||||||
|
}
|
||||||
|
|
||||||
|
mappingsObject.entrySet().forEach { (key, value): Map.Entry<String, JsonElement> ->
|
||||||
|
if (value.isJsonArray) {
|
||||||
|
mappings[key] = gson.fromJson(value, ArrayList::class.java)
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
if (value.isJsonObject) {
|
||||||
|
mappings[key] = gson.fromJson(value, ConcurrentHashMap::class.java)
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
mappings[key] = value.asString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
val mapper = Mapper(*mappers)
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
mapper.loadApk(getApplicationSourceDir() ?: throw Exception("Failed to get APK"))
|
||||||
|
}.onFailure {
|
||||||
|
throw Exception("Failed to load APK", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
measureTimeMillis {
|
||||||
|
val result = mapper.start().apply {
|
||||||
|
addProperty("snap_build_number", snapBuildNumber)
|
||||||
|
}
|
||||||
|
write(result.toString().toByteArray())
|
||||||
|
}.also {
|
||||||
|
Logger.xposedLog("Generated mappings in $it ms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMappedObject(key: String): Any {
|
||||||
|
if (mappings.containsKey(key)) {
|
||||||
|
return mappings[key]!!
|
||||||
|
}
|
||||||
|
throw Exception("No mapping found for $key")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMappedObjectNullable(key: String): Any? {
|
||||||
|
return mappings[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMappedClass(className: String): Class<*> {
|
||||||
|
return context.classLoader.loadClass(getMappedObject(className) as String)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMappedClass(key: String, subKey: String): Class<*> {
|
||||||
|
return context.classLoader.loadClass(getMappedValue(key, subKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMappedValue(key: String): String {
|
||||||
|
return getMappedObject(key) as String
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T : Any> getMappedList(key: String): List<T> {
|
||||||
|
return listOf(getMappedObject(key) as List<T>).flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMappedValue(key: String, subKey: String): String {
|
||||||
|
return getMappedMap(key)[subKey] as String
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun getMappedMap(key: String): Map<String, *> {
|
||||||
|
return getMappedObject(key) as Map<String, *>
|
||||||
|
}
|
||||||
|
}
|