feat(ui): setup activity

- remote side context
- fix float dialogs
- fix choose folder
This commit is contained in:
rhunk 2023-08-03 21:48:23 +02:00
parent 853ceec290
commit 641d66b208
39 changed files with 648 additions and 248 deletions

View File

@ -85,6 +85,8 @@ dependencies {
implementation(libs.androidx.material) implementation(libs.androidx.material)
implementation(libs.androidx.activity.ktx) implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.documentfile)
implementation(libs.gson)
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3") debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
debugImplementation("androidx.compose.ui:ui-tooling-preview:1.4.3") debugImplementation("androidx.compose.ui:ui-tooling-preview:1.4.3")

View File

@ -50,8 +50,7 @@
android:name=".ui.setup.SetupActivity" android:name=".ui.setup.SetupActivity"
android:exported="true" android:exported="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:excludeFromRecents="true"> android:excludeFromRecents="true" />
</activity>
<activity <activity
android:name=".ui.map.MapActivity" android:name=".ui.map.MapActivity"
android:exported="true" android:exported="true"

View File

@ -0,0 +1,94 @@
package me.rhunk.snapenhance
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper
import me.rhunk.snapenhance.core.config.ModConfig
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
import me.rhunk.snapenhance.ui.manager.data.ModMappingsInfo
import me.rhunk.snapenhance.ui.manager.data.SnapchatAppInfo
import me.rhunk.snapenhance.ui.setup.Requirements
import me.rhunk.snapenhance.ui.setup.SetupActivity
import java.lang.ref.WeakReference
import kotlin.system.exitProcess
class RemoteSideContext(
val androidContext: Context
) {
private var _activity: WeakReference<Activity>? = null
var activity: Activity?
get() = _activity?.get()
set(value) { _activity = WeakReference(value) }
val config = ModConfig()
val translation = LocaleWrapper()
val mappings = MappingsWrapper(androidContext)
init {
config.loadFromContext(androidContext)
translation.userLocale = config.locale
translation.loadFromContext(androidContext)
mappings.apply {
loadFromContext(androidContext)
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
)
fun checkForRequirements(overrideRequirements: Int? = null) {
var requirements = overrideRequirements ?: 0
if (!config.wasPresent) {
requirements = requirements or Requirements.FIRST_RUN
}
config.root.downloader.saveFolder.get().let {
if (it.isEmpty() || run {
val documentFile = runCatching { DocumentFile.fromTreeUri(androidContext, Uri.parse(it)) }.getOrNull()
documentFile == null || !documentFile.exists() || !documentFile.canWrite()
}) {
requirements = requirements or Requirements.SAVE_FOLDER
}
}
if (mappings.isMappingsOutdated() || !mappings.isMappingsLoaded()) {
requirements = requirements or Requirements.MAPPINGS
}
if (requirements == 0) return
val currentContext = activity ?: androidContext
Intent(currentContext, SetupActivity::class.java).apply {
putExtra("requirements", requirements)
if (currentContext !is Activity) {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
currentContext.startActivity(this)
return@apply
}
currentContext.startActivityForResult(this, 22)
}
if (currentContext !is Activity) {
exitProcess(0)
}
}
}

View File

@ -0,0 +1,15 @@
package me.rhunk.snapenhance
import android.content.Context
import java.lang.ref.WeakReference
object SharedContextHolder {
private lateinit var _remoteSideContext: WeakReference<RemoteSideContext>
fun remote(context: Context): RemoteSideContext {
if (!::_remoteSideContext.isInitialized || _remoteSideContext.get() == null) {
_remoteSideContext = WeakReference(RemoteSideContext(context.applicationContext))
}
return _remoteSideContext.get()!!
}
}

View File

@ -3,7 +3,9 @@ package me.rhunk.snapenhance.bridge
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import me.rhunk.snapenhance.RemoteSideContext
import me.rhunk.snapenhance.SharedContext import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.SharedContextHolder
import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.bridge.wrapper.MessageLoggerWrapper import me.rhunk.snapenhance.bridge.wrapper.MessageLoggerWrapper
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
@ -11,7 +13,12 @@ import me.rhunk.snapenhance.download.DownloadProcessor
class BridgeService : Service() { class BridgeService : Service() {
private lateinit var messageLoggerWrapper: MessageLoggerWrapper private lateinit var messageLoggerWrapper: MessageLoggerWrapper
private lateinit var remoteSideContext: RemoteSideContext
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder {
remoteSideContext = SharedContextHolder.remote(this).apply {
checkForRequirements()
}
messageLoggerWrapper = MessageLoggerWrapper(getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)).also { it.init() } messageLoggerWrapper = MessageLoggerWrapper(getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)).also { it.init() }
return BridgeBinder() return BridgeBinder()
} }
@ -85,7 +92,7 @@ class BridgeService : Service() {
override fun clearMessageLogger() = messageLoggerWrapper.clearMessages() override fun clearMessageLogger() = messageLoggerWrapper.clearMessages()
override fun fetchTranslations() = LocaleWrapper.fetchLocales(context = this@BridgeService).associate { override fun fetchLocales(userLocale: String) = LocaleWrapper.fetchLocales(context = this@BridgeService, userLocale).associate {
it.locale to it.content it.locale to it.content
} }
@ -98,6 +105,8 @@ class BridgeService : Service() {
} }
override fun enqueueDownload(intent: Intent, callback: DownloadCallback) { override fun enqueueDownload(intent: Intent, callback: DownloadCallback) {
SharedContextHolder.remote(this@BridgeService)
//TODO: refactor shared context
SharedContext.ensureInitialized(this@BridgeService) SharedContext.ensureInitialized(this@BridgeService)
DownloadProcessor(this@BridgeService, callback).onReceive(intent) DownloadProcessor(this@BridgeService, callback).onReceive(intent)
} }

View File

@ -59,10 +59,6 @@ class DownloadProcessor (
private val context: Context, private val context: Context,
private val callback: DownloadCallback private val callback: DownloadCallback
) { ) {
companion object {
const val DOWNLOAD_REQUEST_EXTRA = "request"
const val DOWNLOAD_METADATA_EXTRA = "metadata"
}
private val translation by lazy { private val translation by lazy {
SharedContext.translation.getCategory("download_processor") SharedContext.translation.getCategory("download_processor")
@ -184,7 +180,9 @@ class DownloadProcessor (
fun handleInputStream(inputStream: InputStream) { fun handleInputStream(inputStream: InputStream) {
createMediaTempFile().apply { createMediaTempFile().apply {
if (inputMedia.encryption != null) { if (inputMedia.encryption != null) {
decryptInputStream(inputStream, inputMedia.encryption).use { decryptedInputStream -> decryptInputStream(inputStream,
inputMedia.encryption!!
).use { decryptedInputStream ->
decryptedInputStream.copyTo(outputStream()) decryptedInputStream.copyTo(outputStream())
} }
} else { } else {
@ -286,8 +284,8 @@ class DownloadProcessor (
fun onReceive(intent: Intent) { fun onReceive(intent: Intent) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val downloadMetadata = gson.fromJson(intent.getStringExtra(DOWNLOAD_METADATA_EXTRA)!!, DownloadMetadata::class.java) val downloadMetadata = gson.fromJson(intent.getStringExtra(DownloadManagerClient.DOWNLOAD_METADATA_EXTRA)!!, DownloadMetadata::class.java)
val downloadRequest = gson.fromJson(intent.getStringExtra(DOWNLOAD_REQUEST_EXTRA)!!, DownloadRequest::class.java) val downloadRequest = gson.fromJson(intent.getStringExtra(DownloadManagerClient.DOWNLOAD_REQUEST_EXTRA)!!, DownloadRequest::class.java)
SharedContext.downloadTaskManager.canDownloadMedia(downloadMetadata.mediaIdentifier)?.let { downloadStage -> SharedContext.downloadTaskManager.canDownloadMedia(downloadMetadata.mediaIdentifier)?.let { downloadStage ->
translation[if (downloadStage.isFinalStage) { translation[if (downloadStage.isFinalStage) {

View File

@ -1,43 +1,49 @@
package me.rhunk.snapenhance.ui.manager package me.rhunk.snapenhance.ui.manager
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.remember
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import me.rhunk.snapenhance.SharedContextHolder
import me.rhunk.snapenhance.ui.AppMaterialTheme import me.rhunk.snapenhance.ui.AppMaterialTheme
import me.rhunk.snapenhance.ui.manager.util.SaveFolderChecker
import me.rhunk.snapenhance.util.ActivityResultCallback
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val activityResultCallbacks = mutableMapOf<Int, ActivityResultCallback>()
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val startDestination = intent.getStringExtra("route")?.let { EnumSection.fromRoute(it) } ?: EnumSection.HOME val startDestination = intent.getStringExtra("route")?.let { EnumSection.fromRoute(it) } ?: EnumSection.HOME
val managerContext = ManagerContext(this) val managerContext = SharedContextHolder.remote(this).apply {
activity = this@MainActivity
checkForRequirements()
}
//FIXME: temporary save folder val sections = EnumSection.values().toList().associateWith {
SaveFolderChecker.askForFolder( it.section.constructors.first().call()
this, }.onEach { (section, instance) ->
managerContext.config.root.downloader.saveFolder) with(instance) {
{ enumSection = section
managerContext.config.writeConfig() context = managerContext
init()
}
} }
setContent { setContent {
val navController = rememberNavController() val navController = rememberNavController()
val navigation = Navigation(managerContext) val navigation = remember { Navigation() }
AppMaterialTheme { AppMaterialTheme {
Scaffold( Scaffold(
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
bottomBar = { navigation.NavBar(navController = navController) } bottomBar = { navigation.NavBar(navController = navController) }
) { innerPadding -> ) { innerPadding ->
navigation.NavigationHost(navController = navController, innerPadding = innerPadding, startDestination = startDestination) navigation.NavigationHost(
sections = sections,
navController = navController,
innerPadding = innerPadding,
startDestination = startDestination
)
} }
} }
} }

View File

@ -1,38 +0,0 @@
package me.rhunk.snapenhance.ui.manager
import android.content.Context
import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
import me.rhunk.snapenhance.core.config.ModConfig
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
import me.rhunk.snapenhance.ui.manager.data.ModMappingsInfo
import me.rhunk.snapenhance.ui.manager.data.SnapchatAppInfo
class ManagerContext(
private val context: Context
) {
val config = ModConfig()
val translation = LocaleWrapper()
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
)
}

View File

@ -12,7 +12,6 @@ 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.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
@ -23,25 +22,17 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
class Navigation( class Navigation{
private val context: ManagerContext
) {
@Composable @Composable
fun NavigationHost( fun NavigationHost(
sections: Map<EnumSection, Section>,
startDestination: EnumSection, startDestination: EnumSection,
navController: NavHostController, navController: NavHostController,
innerPadding: PaddingValues innerPadding: PaddingValues
) { ) {
val sections = remember { EnumSection.values().toList().map {
it to it.section.constructors.first().call()
}.onEach { (section, instance) ->
instance.enumSection = section
instance.manager = context
instance.navController = navController
} }
NavHost(navController, startDestination = startDestination.route, Modifier.padding(innerPadding)) { NavHost(navController, startDestination = startDestination.route, Modifier.padding(innerPadding)) {
sections.forEach { (_, instance) -> sections.forEach { (_, instance) ->
instance.navController = navController
instance.build(this) instance.build(this)
} }
} }

View File

@ -11,6 +11,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import me.rhunk.snapenhance.RemoteSideContext
import me.rhunk.snapenhance.ui.manager.sections.HomeSection import me.rhunk.snapenhance.ui.manager.sections.HomeSection
import me.rhunk.snapenhance.ui.manager.sections.NotImplemented import me.rhunk.snapenhance.ui.manager.sections.NotImplemented
import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection
@ -61,9 +62,11 @@ enum class EnumSection(
open class Section { open class Section {
lateinit var enumSection: EnumSection lateinit var enumSection: EnumSection
lateinit var manager: ManagerContext lateinit var context: RemoteSideContext
lateinit var navController: NavController lateinit var navController: NavController
open fun init() {}
@Composable @Composable
open fun Content() { NotImplemented() } open fun Content() { NotImplemented() }

View File

@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.manager.Section
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
import me.rhunk.snapenhance.ui.setup.Requirements
class HomeSection : Section() { class HomeSection : Section() {
companion object { companion object {
@ -76,7 +77,9 @@ class HomeSection : Section() {
) )
//inline button //inline button
Button(onClick = {}, modifier = Modifier.height(40.dp)) { Button(onClick = {
context.checkForRequirements(Requirements.MAPPINGS)
}, modifier = Modifier.height(40.dp)) {
Icon(Icons.Filled.Refresh, contentDescription = "Refresh") Icon(Icons.Filled.Refresh, contentDescription = "Refresh")
} }
} }
@ -102,7 +105,7 @@ class HomeSection : Section() {
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) )
SummaryCards(manager.getInstallationSummary()) SummaryCards(context.getInstallationSummary())
} }
} }
} }

View File

@ -140,14 +140,22 @@ class Dialogs {
Text(text = "Cancel") Text(text = "Cancel")
} }
Button(onClick = { Button(onClick = {
if (property.key.dataType.type == DataProcessors.Type.INTEGER) { when (property.key.dataType.type) {
runCatching { DataProcessors.Type.INTEGER -> {
property.value.setAny(fieldValue.value.text.toInt()) runCatching {
}.onFailure { property.value.setAny(fieldValue.value.text.toInt())
property.value.setAny(0) }.onFailure {
property.value.setAny(0)
}
} }
} else { DataProcessors.Type.FLOAT -> {
property.value.setAny(fieldValue.value.text) runCatching {
property.value.setAny(fieldValue.value.text.toFloat())
}.onFailure {
property.value.setAny(0f)
}
}
else -> property.value.setAny(fieldValue.value.text)
} }
dismiss() dismiss()
}) { }) {

View File

@ -1,5 +1,6 @@
package me.rhunk.snapenhance.ui.manager.sections.features package me.rhunk.snapenhance.ui.manager.sections.features
import androidx.activity.ComponentActivity
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -20,6 +21,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.rounded.Save import androidx.compose.material.icons.rounded.Save
import androidx.compose.material3.Card import androidx.compose.material3.Card
@ -55,6 +57,7 @@ import me.rhunk.snapenhance.core.config.ConfigContainer
import me.rhunk.snapenhance.core.config.DataProcessors import me.rhunk.snapenhance.core.config.DataProcessors
import me.rhunk.snapenhance.core.config.PropertyPair import me.rhunk.snapenhance.core.config.PropertyPair
import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.manager.Section
import me.rhunk.snapenhance.ui.util.ChooseFolderHelper
class FeaturesSection : Section() { class FeaturesSection : Section() {
private val dialogs by lazy { Dialogs() } private val dialogs by lazy { Dialogs() }
@ -63,6 +66,15 @@ class FeaturesSection : Section() {
private const val MAIN_ROUTE = "root" private const val MAIN_ROUTE = "root"
} }
private lateinit var openFolderCallback: (uri: String) -> Unit
private lateinit var openFolderLauncher: () -> Unit
override fun init() {
openFolderLauncher = ChooseFolderHelper.createChooseFolder(context.activity!! as ComponentActivity) {
openFolderCallback(it)
}
}
@Composable @Composable
private fun PropertyAction(property: PropertyPair<*>, registerClickCallback: RegisterClickCallback) { private fun PropertyAction(property: PropertyPair<*>, registerClickCallback: RegisterClickCallback) {
val showDialog = remember { mutableStateOf(false) } val showDialog = remember { mutableStateOf(false) }
@ -83,6 +95,18 @@ class FeaturesSection : Section() {
val propertyValue = property.value val propertyValue = property.value
if (property.key.params.isFolder) {
IconButton(onClick = registerClickCallback {
openFolderCallback = { uri ->
propertyValue.setAny(uri)
}
openFolderLauncher()
}.let { { it.invoke(true) } }) {
Icon(Icons.Filled.FolderOpen, contentDescription = null)
}
return
}
when (val dataType = remember { property.key.dataType.type }) { when (val dataType = remember { property.key.dataType.type }) {
DataProcessors.Type.BOOLEAN -> { DataProcessors.Type.BOOLEAN -> {
val state = remember { mutableStateOf(propertyValue.get() as Boolean) } val state = remember { mutableStateOf(propertyValue.get() as Boolean) }
@ -116,7 +140,7 @@ class FeaturesSection : Section() {
DataProcessors.Type.STRING_MULTIPLE_SELECTION -> { DataProcessors.Type.STRING_MULTIPLE_SELECTION -> {
dialogs.MultipleSelectionDialog(property) dialogs.MultipleSelectionDialog(property)
} }
DataProcessors.Type.STRING, DataProcessors.Type.INTEGER -> { DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> {
dialogs.KeyboardInputDialog(property) { showDialog.value = false } dialogs.KeyboardInputDialog(property) { showDialog.value = false }
} }
else -> {} else -> {}
@ -124,7 +148,8 @@ class FeaturesSection : Section() {
} }
registerDialogOnClickCallback().let { { it.invoke(true) } }.also { registerDialogOnClickCallback().let { { it.invoke(true) } }.also {
if (dataType == DataProcessors.Type.INTEGER || dataType == DataProcessors.Type.FLOAT) { if (dataType == DataProcessors.Type.INTEGER ||
dataType == DataProcessors.Type.FLOAT) {
FilledIconButton(onClick = it) { FilledIconButton(onClick = it) {
Text( Text(
text = propertyValue.get().toString(), text = propertyValue.get().toString(),
@ -256,7 +281,7 @@ class FeaturesSection : Section() {
floatingActionButton = { floatingActionButton = {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
manager.config.writeConfig() context.config.writeConfig()
scope.launch { scope.launch {
scaffoldState.snackbarHostState.showSnackbar("Saved") scaffoldState.snackbarHostState.showSnackbar("Saved")
} }
@ -299,13 +324,13 @@ class FeaturesSection : Section() {
} }
} }
} }
queryContainerRecursive(manager.config.root) queryContainerRecursive(context.config.root)
containers containers
} }
navGraphBuilder.navigation(route = "features", startDestination = MAIN_ROUTE) { navGraphBuilder.navigation(route = "features", startDestination = MAIN_ROUTE) {
composable(MAIN_ROUTE) { composable(MAIN_ROUTE) {
Container(MAIN_ROUTE, manager.config.root) Container(MAIN_ROUTE, context.config.root)
} }
composable("container/{name}") { backStackEntry -> composable("container/{name}") { backStackEntry ->

View File

@ -1,42 +0,0 @@
package me.rhunk.snapenhance.ui.manager.util
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import me.rhunk.snapenhance.core.config.PropertyValue
import kotlin.system.exitProcess
object SaveFolderChecker {
fun askForFolder(activity: ComponentActivity, property: PropertyValue<String>, saveConfig: () -> Unit) {
if (property.get().isEmpty() || !property.get().startsWith("content://")) {
val startActivity = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) result@{
if (it.resultCode != Activity.RESULT_OK) return@result
val uri = it.data?.data ?: return@result
val value = uri.toString()
activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
property.set(value)
saveConfig()
Toast.makeText(activity, "save folder set!", Toast.LENGTH_SHORT).show()
activity.finish()
}
AlertDialog.Builder(activity)
.setTitle("Save folder")
.setMessage("Please select a folder where you want to save downloaded files.")
.setPositiveButton("Select") { _, _ ->
startActivity.launch(
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
)
}
.setNegativeButton("Cancel") { _, _ ->
exitProcess(0)
}
.show()
}
}
}

View File

@ -1,33 +1,21 @@
package me.rhunk.snapenhance.ui.setup package me.rhunk.snapenhance.ui.setup
import android.os.Bundle object Requirements {
const val FIRST_RUN = 0b00001
const val LANGUAGE = 0b00010
const val MAPPINGS = 0b00100
const val SAVE_FOLDER = 0b01000
const val FFMPEG = 0b10000
data class Requirements( fun getName(requirement: Int): String {
val firstRun: Boolean = false, return when (requirement) {
val language: Boolean = false, FIRST_RUN -> "FIRST_RUN"
val mappings: Boolean = false, LANGUAGE -> "LANGUAGE"
val saveFolder: Boolean = false, MAPPINGS -> "MAPPINGS"
val ffmpeg: Boolean = false SAVE_FOLDER -> "SAVE_FOLDER"
) { FFMPEG -> "FFMPEG"
companion object { else -> "UNKNOWN"
fun fromBundle(bundle: Bundle): Requirements {
return Requirements(
firstRun = bundle.getBoolean("firstRun"),
language = bundle.getBoolean("language"),
mappings = bundle.getBoolean("mappings"),
saveFolder = bundle.getBoolean("saveFolder"),
ffmpeg = bundle.getBoolean("ffmpeg")
)
}
fun toBundle(requirements: Requirements): Bundle {
return Bundle().apply {
putBoolean("firstRun", requirements.firstRun)
putBoolean("language", requirements.language)
putBoolean("mappings", requirements.mappings)
putBoolean("saveFolder", requirements.saveFolder)
putBoolean("ffmpeg", requirements.ffmpeg)
}
} }
} }
} }

View File

@ -1,7 +1,9 @@
package me.rhunk.snapenhance.ui.setup package me.rhunk.snapenhance.ui.setup
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -14,6 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForwardIos import androidx.compose.material.icons.filled.ArrowForwardIos
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -28,48 +31,64 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import me.rhunk.snapenhance.SharedContextHolder
import me.rhunk.snapenhance.ui.AppMaterialTheme import me.rhunk.snapenhance.ui.AppMaterialTheme
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen import me.rhunk.snapenhance.ui.setup.screens.SetupScreen
import me.rhunk.snapenhance.ui.setup.screens.impl.FfmpegScreen import me.rhunk.snapenhance.ui.setup.screens.impl.FfmpegScreen
import me.rhunk.snapenhance.ui.setup.screens.impl.LanguageScreen
import me.rhunk.snapenhance.ui.setup.screens.impl.MappingsScreen import me.rhunk.snapenhance.ui.setup.screens.impl.MappingsScreen
import me.rhunk.snapenhance.ui.setup.screens.impl.PickLanguageScreen
import me.rhunk.snapenhance.ui.setup.screens.impl.SaveFolderScreen import me.rhunk.snapenhance.ui.setup.screens.impl.SaveFolderScreen
import me.rhunk.snapenhance.ui.setup.screens.impl.WelcomeScreen
class SetupActivity : ComponentActivity() { class SetupActivity : ComponentActivity() {
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val requirements = intent.getBundleExtra("requirements")?.let { val setupContext = SharedContextHolder.remote(this).apply {
Requirements.fromBundle(it) activity = this@SetupActivity
} ?: Requirements(firstRun = true) }
val requirements = intent.getIntExtra("requirements", Requirements.FIRST_RUN)
fun hasRequirement(requirement: Int) = requirements and requirement == requirement
val requiredScreens = mutableListOf<SetupScreen>() val requiredScreens = mutableListOf<SetupScreen>()
with(requiredScreens) { with(requiredScreens) {
with(requirements) { val isFirstRun = hasRequirement(Requirements.FIRST_RUN)
if (firstRun || language) add(LanguageScreen().apply { route = "language" }) if (isFirstRun || hasRequirement(Requirements.LANGUAGE)) {
if (firstRun) add(WelcomeScreen().apply { route = "welcome" }) add(PickLanguageScreen().apply { route = "language" })
if (firstRun || saveFolder) add(SaveFolderScreen().apply { route = "saveFolder" }) }
if (firstRun || mappings) add(MappingsScreen().apply { route = "mappings" }) if (isFirstRun || hasRequirement(Requirements.SAVE_FOLDER)) {
if (firstRun || ffmpeg) add(FfmpegScreen().apply { route = "ffmpeg" }) add(SaveFolderScreen().apply { route = "saveFolder" })
}
if (isFirstRun || hasRequirement(Requirements.MAPPINGS)) {
add(MappingsScreen().apply { route = "mappings" })
}
if (isFirstRun || hasRequirement(Requirements.FFMPEG)) {
add(FfmpegScreen().apply { route = "ffmpeg" })
} }
} }
// If there are no required screens, we can just finish the activity
if (requiredScreens.isEmpty()) { if (requiredScreens.isEmpty()) {
finish() finish()
return return
} }
requiredScreens.forEach { screen ->
screen.context = setupContext
screen.init()
}
setContent { setContent {
val navController = rememberNavController() val navController = rememberNavController()
val canGoNext = remember { mutableStateOf(false) } val canGoNext = remember { mutableStateOf(false) }
fun nextScreen() { fun nextScreen() {
if (!canGoNext.value) return if (!canGoNext.value) return
canGoNext.value = false
if (requiredScreens.size > 1) { if (requiredScreens.size > 1) {
canGoNext.value = false
requiredScreens.removeFirst() requiredScreens.removeFirst()
navController.navigate(requiredScreens.first().route) navController.navigate(requiredScreens.first().route)
} else { } else {
@ -98,18 +117,21 @@ class SetupActivity : ComponentActivity() {
.alpha(alpha) .alpha(alpha)
) { ) {
Icon( Icon(
imageVector = Icons.Default.ArrowForwardIos, imageVector = if (requiredScreens.size <= 1 && canGoNext.value) {
Icons.Default.Check
} else {
Icons.Default.ArrowForwardIos
},
contentDescription = null contentDescription = null
) )
} }
} }
}, },
) { paddingValues -> ) {
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)
.fillMaxSize() .fillMaxSize()
.padding(paddingValues)
) { ) {
NavHost( NavHost(
navController = navController, navController = navController,
@ -118,6 +140,7 @@ class SetupActivity : ComponentActivity() {
requiredScreens.forEach { screen -> requiredScreens.forEach { screen ->
screen.allowNext = { canGoNext.value = it } screen.allowNext = { canGoNext.value = it }
composable(screen.route) { composable(screen.route) {
BackHandler(true) {}
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,

View File

@ -1,14 +0,0 @@
package me.rhunk.snapenhance.ui.setup
import android.content.Context
import me.rhunk.snapenhance.core.config.ModConfig
class SetupContext(
private val context: Context
) {
val config = ModConfig()
init {
config.loadFromContext(context)
}
}

View File

@ -1,11 +1,31 @@
package me.rhunk.snapenhance.ui.setup.screens package me.rhunk.snapenhance.ui.setup.screens
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.rhunk.snapenhance.RemoteSideContext
abstract class SetupScreen { abstract class SetupScreen {
lateinit var context: RemoteSideContext
lateinit var allowNext: (Boolean) -> Unit lateinit var allowNext: (Boolean) -> Unit
lateinit var route: String lateinit var route: String
@Composable
fun DialogText(text: String, modifier: Modifier = Modifier) {
Text(
text = text,
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
modifier = Modifier.padding(16.dp).then(modifier)
)
}
open fun init() {}
@Composable @Composable
abstract fun Content() abstract fun Content()
} }

View File

@ -1,11 +0,0 @@
package me.rhunk.snapenhance.ui.setup.screens.impl
import androidx.compose.runtime.Composable
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen
class LanguageScreen : SetupScreen(){
@Composable
override fun Content() {
}
}

View File

@ -1,16 +1,98 @@
package me.rhunk.snapenhance.ui.setup.screens.impl package me.rhunk.snapenhance.ui.setup.screens.impl
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen import me.rhunk.snapenhance.ui.setup.screens.SetupScreen
class MappingsScreen : SetupScreen() { class MappingsScreen : SetupScreen() {
@Composable @Composable
override fun Content() { override fun Content() {
Text(text = "Mappings") val coroutineScope = rememberCoroutineScope()
Button(onClick = { allowNext(true) }) { val infoText = remember { mutableStateOf(null as String?) }
Text(text = "Next") val isGenerating = remember { mutableStateOf(false) }
if (infoText.value != null) {
Dialog(onDismissRequest = {
infoText.value = null
}) {
Surface(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
color = MaterialTheme.colors.surface,
shape = RoundedCornerShape(16.dp),
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = infoText.value!!)
Button(onClick = {
infoText.value = null
},
modifier = Modifier.padding(top = 5.dp).align(alignment = androidx.compose.ui.Alignment.End)) {
Text(text = "OK")
}
}
}
}
}
fun tryToGenerateMappings() {
//check for snapchat installation
val installationSummary = context.getInstallationSummary()
if (installationSummary.snapchatInfo == null) {
throw Exception(context.translation["setup.mappings.generate_failure_no_snapchat"])
}
with(context.mappings) {
refresh()
}
}
val hasMappings = remember { mutableStateOf(false) }
DialogText(text = context.translation["setup.mappings.dialog"])
if (hasMappings.value) return
Button(onClick = {
if (isGenerating.value) return@Button
isGenerating.value = true
coroutineScope.launch(Dispatchers.IO) {
runCatching {
tryToGenerateMappings()
allowNext(true)
infoText.value = context.translation["setup.mappings.generate_success"]
hasMappings.value = true
}.onFailure {
isGenerating.value = false
infoText.value = context.translation["setup.mappings.generate_failure"] + "\n\n" + it.message
Logger.error("Failed to generate mappings", it)
}
}
}) {
if (isGenerating.value) {
CircularProgressIndicator(
modifier = Modifier.padding(end = 5.dp).size(25.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colors.onPrimary
)
} else {
Text(text = context.translation["setup.mappings.generate_button"])
}
} }
} }
} }

View File

@ -0,0 +1,115 @@
package me.rhunk.snapenhance.ui.setup.screens.impl
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
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.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
import me.rhunk.snapenhance.ui.util.ObservableMutableState
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen
import java.util.Locale
class PickLanguageScreen : SetupScreen(){
@Composable
override fun Content() {
val androidContext = LocalContext.current
val availableLocales = remember { LocaleWrapper.fetchAvailableLocales(androidContext) }
allowNext(true)
fun getLocaleDisplayName(locale: String): String {
locale.split("_").let {
return java.util.Locale(it[0], it[1]).getDisplayName(java.util.Locale.getDefault())
}
}
val selectedLocale = remember {
val deviceLocale = Locale.getDefault().toString()
fun reloadTranslation(selectedLocale: String) {
context.translation.reloadFromContext(androidContext, selectedLocale)
}
ObservableMutableState(
defaultValue = availableLocales.firstOrNull {
locale -> locale == deviceLocale
} ?: LocaleWrapper.DEFAULT_LOCALE
) { _, newValue ->
context.config.locale = newValue
context.config.writeConfig()
reloadTranslation(newValue)
}.also { reloadTranslation(it.value) }
}
DialogText(text = context.translation["setup.dialogs.select_language"])
val isDialog = remember { mutableStateOf(false) }
if (isDialog.value) {
Dialog(onDismissRequest = { isDialog.value = false }) {
Surface(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
elevation = 8.dp,
shape = MaterialTheme.shapes.medium
) {
LazyColumn(
modifier = Modifier.scrollable(rememberScrollState(), orientation = Orientation.Vertical)
) {
items(availableLocales) { locale ->
Box(
modifier = Modifier
.height(70.dp)
.fillMaxWidth()
.clickable {
selectedLocale.value = locale
isDialog.value = false
},
contentAlignment = Alignment.Center
) {
Text(
text = getLocaleDisplayName(locale),
fontSize = 16.sp,
fontWeight = FontWeight.Light,
)
}
}
}
}
}
}
Box(
modifier = Modifier
.padding(top = 40.dp)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
OutlinedButton(onClick = {
isDialog.value = true
}) {
Text(text = getLocaleDisplayName(selectedLocale.value), fontSize = 16.sp, fontWeight = FontWeight.Light)
}
}
}
}

View File

@ -1,17 +1,47 @@
package me.rhunk.snapenhance.ui.setup.screens.impl package me.rhunk.snapenhance.ui.setup.screens.impl
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.ui.util.ObservableMutableState
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen import me.rhunk.snapenhance.ui.setup.screens.SetupScreen
import me.rhunk.snapenhance.ui.util.ChooseFolderHelper
class SaveFolderScreen : SetupScreen() { class SaveFolderScreen : SetupScreen() {
private lateinit var saveFolder: ObservableMutableState<String>
private lateinit var openFolderLauncher: () -> Unit
override fun init() {
saveFolder = ObservableMutableState(
defaultValue = "",
onChange = { _, newValue ->
Logger.debug(newValue)
if (newValue.isNotBlank()) {
context.config.root.downloader.saveFolder.set(newValue)
context.config.writeConfig()
allowNext(true)
}
}
)
openFolderLauncher = ChooseFolderHelper.createChooseFolder(context.activity as ComponentActivity) { uri ->
saveFolder.value = uri
}
}
@Composable @Composable
override fun Content() { override fun Content() {
Text(text = "SaveFolder") DialogText(text = context.translation["setup.dialogs.save_folder"])
Button(onClick = {allowNext(true)}) { Spacer(modifier = Modifier.height(16.dp))
Text(text = "Next") Button(onClick = {
openFolderLauncher()
}) {
Text(text = context.translation["setup.dialogs.select_save_folder_button"])
} }
} }
} }

View File

@ -0,0 +1,26 @@
package me.rhunk.snapenhance.ui.util
import android.app.Activity
import android.content.Intent
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
object ChooseFolderHelper {
fun createChooseFolder(activity: ComponentActivity, callback: (uri: String) -> Unit): () -> Unit {
val activityResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) result@{
if (it.resultCode != Activity.RESULT_OK) return@result
val uri = it.data?.data ?: return@result
val value = uri.toString()
activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
callback(value)
}
return {
activityResultLauncher.launch(
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
)
}
}
}

View File

@ -0,0 +1,19 @@
package me.rhunk.snapenhance.ui.util
import androidx.compose.runtime.MutableState
class ObservableMutableState<T>(
defaultValue: T,
inline val onChange: (T, T) -> Unit = { _, _ -> },
) : MutableState<T> {
private var mutableValue: T = defaultValue
override var value: T
get() = mutableValue
set(value) {
val oldValue = mutableValue
mutableValue = value
onChange(oldValue, value)
}
override fun component1() = value
override fun component2(): (T) -> Unit = { value = it }
}

View File

@ -83,11 +83,11 @@ interface BridgeInterface {
void clearMessageLogger(); void clearMessageLogger();
/** /**
* Fetch the translations * Fetch the locales
* *
* @return the translations result * @return the locale result
*/ */
Map<String, String> fetchTranslations(); Map<String, String> fetchLocales(String userLocale);
/** /**
* Get check for updates last time * Get check for updates last time

View File

@ -1,4 +1,21 @@
{ {
"setup": {
"dialogs": {
"select_language": "Select Language",
"save_folder": "For downloading snapchat media, you'll need to choose a save location. This can be changed later in the application settings.",
"select_save_folder_button": "Select Save Folder",
"mappings": "To support a wide range of versions, mappings need to be generated for the current snapchat version."
},
"mappings": {
"dialog": "To support a wide range of versions, mappings need to be generated for the current snapchat version.",
"snapchat_not_found": "Snapchat could not be found on your device. Please install Snapchat and try again.",
"snapchat_not_supported": "Snapchat is not supported. Please update Snapchat and try again.",
"generate_button": "Generate",
"generate_error": "An error occurred while generating mappings. Please try again.",
"generate_success": "Mappings generated successfully."
}
},
"category": { "category": {
"spying_privacy": "Spying & Privacy", "spying_privacy": "Spying & Privacy",
"media_manager": "Media Manager", "media_manager": "Media Manager",

View File

@ -1,4 +1,12 @@
{ {
"setup": {
"dialogs": {
"select_language": "Selectionner une langue",
"save_folder": "Pour télécharger les médias Snapchat, vous devez choisir un emplacement de sauvegarde. Cela peut être modifié plus tard dans les paramètres de l'application.",
"select_save_folder_button": "Choisir un emplacement de sauvegarde"
}
},
"category": { "category": {
"spying_privacy": "Espionnage et vie privée", "spying_privacy": "Espionnage et vie privée",
"media_manager": "Gestionnaire de média", "media_manager": "Gestionnaire de média",

View File

@ -16,6 +16,11 @@ object Logger {
Log.d(TAG, message.toString()) Log.d(TAG, message.toString())
} }
fun debug(tag: String, message: Any?) {
if (!BuildConfig.DEBUG) return
Log.d(tag, message.toString())
}
fun error(throwable: Throwable) { fun error(throwable: Throwable) {
Log.e(TAG, "", throwable) Log.e(TAG, "", throwable)
} }

View File

@ -122,4 +122,8 @@ class ModContext {
fun reloadConfig() { fun reloadConfig() {
modConfig.loadFromBridge(bridgeClient) modConfig.loadFromBridge(bridgeClient)
} }
fun getConfigLocale(): String {
return modConfig.locale
}
} }

View File

@ -90,14 +90,14 @@ class SnapEnhance {
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
private suspend fun init() { private suspend fun init() {
//load translations in a coroutine to speed up initialization
withContext(appContext.coroutineDispatcher) {
appContext.translation.loadFromBridge(appContext.bridgeClient)
}
measureTime { measureTime {
with(appContext) { with(appContext) {
reloadConfig() reloadConfig()
withContext(appContext.coroutineDispatcher) {
translation.userLocale = getConfigLocale()
translation.loadFromBridge(appContext.bridgeClient)
}
mappings.init() mappings.init()
eventDispatcher.init() eventDispatcher.init()
//if mappings aren't loaded, we can't initialize features //if mappings aren't loaded, we can't initialize features

View File

@ -36,8 +36,9 @@ class BridgeClient(
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
) )
//TODO: randomize package name
val intent = Intent() val intent = Intent()
.setClassName(BuildConfig.APPLICATION_ID, BridgeService::class.java.name) .setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.bridge.BridgeService")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
bindService( bindService(
intent, intent,
@ -103,7 +104,7 @@ class BridgeClient(
fun clearMessageLogger() = service.clearMessageLogger() fun clearMessageLogger() = service.clearMessageLogger()
fun fetchTranslations() = service.fetchTranslations().map { fun fetchLocales(userLocale: String) = service.fetchLocales(userLocale).map {
LocalePair(it.key, it.value) LocalePair(it.key, it.value)
} }

View File

@ -13,15 +13,14 @@ class LocaleWrapper {
companion object { companion object {
const val DEFAULT_LOCALE = "en_US" const val DEFAULT_LOCALE = "en_US"
fun fetchLocales(context: Context): List<LocalePair> { fun fetchLocales(context: Context, locale: String = DEFAULT_LOCALE): List<LocalePair> {
val deviceLocale = Locale.getDefault().toString() val locales = mutableListOf<LocalePair>().apply {
val locales = mutableListOf<LocalePair>() add(LocalePair(DEFAULT_LOCALE, context.resources.assets.open("lang/$DEFAULT_LOCALE.json").bufferedReader().use { it.readText() }))
}
locales.add(LocalePair(DEFAULT_LOCALE, context.resources.assets.open("lang/$DEFAULT_LOCALE.json").bufferedReader().use { it.readText() })) if (locale == DEFAULT_LOCALE) return locales
if (deviceLocale == DEFAULT_LOCALE) return locales val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(locale) }?.substring(0, 5) ?: return locales
val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(deviceLocale) }?.substring(0, 5) ?: return locales
context.resources.assets.open("lang/$compatibleLocale.json").use { inputStream -> context.resources.assets.open("lang/$compatibleLocale.json").use { inputStream ->
locales.add(LocalePair(compatibleLocale, inputStream.bufferedReader().use { it.readText() })) locales.add(LocalePair(compatibleLocale, inputStream.bufferedReader().use { it.readText() }))
@ -29,19 +28,24 @@ class LocaleWrapper {
return locales return locales
} }
fun fetchAvailableLocales(context: Context): List<String> {
return context.resources.assets.list("lang")?.map { it.substring(0, 5) } ?: listOf()
}
} }
var userLocale = DEFAULT_LOCALE
private val translationMap = linkedMapOf<String, String>() private val translationMap = linkedMapOf<String, String>()
private lateinit var _locale: String private lateinit var _loadedLocaleString: String
val locale by lazy { val loadedLocale by lazy {
Locale(_locale.substring(0, 2), _locale.substring(3, 5)) Locale(_loadedLocaleString.substring(0, 2), _loadedLocaleString.substring(3, 5))
} }
private fun load(localePair: LocalePair) { private fun load(localePair: LocalePair) {
if (!::_locale.isInitialized) { if (!::_loadedLocaleString.isInitialized) {
_locale = localePair.locale _loadedLocaleString = localePair.locale
} }
val translations = JsonParser.parseString(localePair.content).asJsonObject val translations = JsonParser.parseString(localePair.content).asJsonObject
@ -64,17 +68,23 @@ class LocaleWrapper {
} }
fun loadFromBridge(bridgeClient: BridgeClient) { fun loadFromBridge(bridgeClient: BridgeClient) {
bridgeClient.fetchTranslations().forEach { bridgeClient.fetchLocales(userLocale).forEach {
load(it) load(it)
} }
} }
fun loadFromContext(context: Context) { fun loadFromContext(context: Context) {
fetchLocales(context).forEach { fetchLocales(context, userLocale).forEach {
load(it) load(it)
} }
} }
fun reloadFromContext(context: Context, locale: String) {
userLocale = locale
translationMap.clear()
loadFromContext(context)
}
operator fun get(key: String): String { operator fun get(key: String): String {
return translationMap[key] ?: key.also { Logger.debug("Missing translation for $key") } return translationMap[key] ?: key.also { Logger.debug("Missing translation for $key") }
} }

View File

@ -46,7 +46,6 @@ class MappingsWrapper(
private val mappings = ConcurrentHashMap<String, Any>() private val mappings = ConcurrentHashMap<String, Any>()
private var snapBuildNumber: Long = 0 private var snapBuildNumber: Long = 0
@Suppress("deprecation")
fun init() { fun init() {
snapBuildNumber = getSnapchatVersionCode() snapBuildNumber = getSnapchatVersionCode()
@ -54,6 +53,7 @@ class MappingsWrapper(
runCatching { runCatching {
loadCached() loadCached()
}.onFailure { }.onFailure {
Logger.error("Failed to load cached mappings", it)
delete() delete()
} }
} }
@ -100,6 +100,7 @@ class MappingsWrapper(
} }
fun refresh() { fun refresh() {
snapBuildNumber = getSnapchatVersionCode()
val mapper = Mapper(*mappers) val mapper = Mapper(*mappers)
runCatching { runCatching {
@ -114,7 +115,7 @@ class MappingsWrapper(
} }
write(result.toString().toByteArray()) write(result.toString().toByteArray())
}.also { }.also {
Logger.xposedLog("Generated mappings in $it ms") Logger.debug("Generated mappings in $it ms")
} }
} }

View File

@ -38,7 +38,7 @@ open class ConfigContainer(
vararg values: String = emptyArray(), vararg values: String = emptyArray(),
params: ConfigParamsBuilder = {} params: ConfigParamsBuilder = {}
) = registerProperty(key, ) = registerProperty(key,
DataProcessors.STRING_MULTIPLE_SELECTION, PropertyValue(emptyList<String>(), defaultValues = values.toList()), params) DataProcessors.STRING_MULTIPLE_SELECTION, PropertyValue(mutableListOf<String>(), defaultValues = values.toList()), params)
//null value is considered as Off/Disabled //null value is considered as Off/Disabled
protected fun unique( protected fun unique(

View File

@ -10,22 +10,20 @@ import me.rhunk.snapenhance.bridge.FileLoaderWrapper
import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
import me.rhunk.snapenhance.core.config.impl.RootConfig import me.rhunk.snapenhance.core.config.impl.RootConfig
import kotlin.properties.Delegates
class ModConfig { class ModConfig {
var locale: String = LocaleWrapper.DEFAULT_LOCALE var locale: String = LocaleWrapper.DEFAULT_LOCALE
set(value) {
field = value
writeConfig()
}
private val gson: Gson = GsonBuilder().setPrettyPrinting().create() private val gson: Gson = GsonBuilder().setPrettyPrinting().create()
private val file = FileLoaderWrapper(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8)) private val file = FileLoaderWrapper(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8))
var wasPresent by Delegates.notNull<Boolean>()
val root = RootConfig() val root = RootConfig()
operator fun getValue(thisRef: Any?, property: Any?) = root operator fun getValue(thisRef: Any?, property: Any?) = root
private fun load() { private fun load() {
wasPresent = file.isFileExists()
if (!file.isFileExists()) { if (!file.isFileExists()) {
writeConfig() writeConfig()
return return
@ -42,12 +40,13 @@ class ModConfig {
private fun loadConfig() { private fun loadConfig() {
val configFileContent = file.read() val configFileContent = file.read()
val configObject = gson.fromJson(configFileContent.toString(Charsets.UTF_8), JsonObject::class.java) val configObject = gson.fromJson(configFileContent.toString(Charsets.UTF_8), JsonObject::class.java)
locale = configObject.get("language")?.asString ?: LocaleWrapper.DEFAULT_LOCALE locale = configObject.get("_locale")?.asString ?: LocaleWrapper.DEFAULT_LOCALE
root.fromJson(configObject)
} }
fun writeConfig() { fun writeConfig() {
val configObject = root.toJson() val configObject = root.toJson()
configObject.addProperty("language", locale) configObject.addProperty("_locale", locale)
file.write(configObject.toString().toByteArray(Charsets.UTF_8)) file.write(configObject.toString().toByteArray(Charsets.UTF_8))
} }

View File

@ -16,11 +16,16 @@ class DownloadManagerClient (
private val metadata: DownloadMetadata, private val metadata: DownloadMetadata,
private val callback: DownloadCallback private val callback: DownloadCallback
) { ) {
companion object {
const val DOWNLOAD_REQUEST_EXTRA = "request"
const val DOWNLOAD_METADATA_EXTRA = "metadata"
}
private fun enqueueDownloadRequest(request: DownloadRequest) { private fun enqueueDownloadRequest(request: DownloadRequest) {
context.bridgeClient.enqueueDownload(Intent().apply { context.bridgeClient.enqueueDownload(Intent().apply {
putExtras(Bundle().apply { putExtras(Bundle().apply {
putString(DownloadProcessor.DOWNLOAD_REQUEST_EXTRA, context.gson.toJson(request)) putString(DOWNLOAD_REQUEST_EXTRA, context.gson.toJson(request))
putString(DownloadProcessor.DOWNLOAD_METADATA_EXTRA, context.gson.toJson(metadata)) putString(DOWNLOAD_METADATA_EXTRA, context.gson.toJson(metadata))
}) })
}, callback) }, callback)
} }

View File

@ -3,9 +3,9 @@ package me.rhunk.snapenhance.features
object FeatureLoadParams { object FeatureLoadParams {
const val NO_INIT = 0 const val NO_INIT = 0
const val INIT_SYNC = 1 const val INIT_SYNC = 0b0001
const val ACTIVITY_CREATE_SYNC = 2 const val ACTIVITY_CREATE_SYNC = 0b0010
const val INIT_ASYNC = 3 const val INIT_ASYNC = 0b0100
const val ACTIVITY_CREATE_ASYNC = 4 const val ACTIVITY_CREATE_ASYNC = 0b1000
} }

View File

@ -84,7 +84,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
${birthday.getDisplayName( ${birthday.getDisplayName(
Calendar.MONTH, Calendar.MONTH,
Calendar.LONG, Calendar.LONG,
context.translation.locale context.translation.loadedLocale
)?.let { )?.let {
context.translation.format("profile_info.birthday", context.translation.format("profile_info.birthday",
"month" to it, "month" to it,

View File

@ -47,9 +47,8 @@ class SettingsGearInjector : AbstractMenu() {
setOnClickListener { setOnClickListener {
val intent = Intent().apply { val intent = Intent().apply {
setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.manager.MainActivity") setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.ui.manager.MainActivity")
putExtra("route", "features") putExtra("route", "features")
putExtra("lspatched", File(context.cacheDir, "lspatch/origin").exists())
} }
context.startActivity(intent) context.startActivity(intent)
} }