diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ad029fc3..5ead87a7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -85,6 +85,8 @@ dependencies { implementation(libs.androidx.material) implementation(libs.androidx.activity.ktx) 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-preview:1.4.3") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 151fd648..e7f62248 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -50,8 +50,7 @@ android:name=".ui.setup.SetupActivity" android:exported="true" android:theme="@style/AppTheme" - android:excludeFromRecents="true"> - + android:excludeFromRecents="true" /> ? = 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/SharedContextHolder.kt b/app/src/main/kotlin/me/rhunk/snapenhance/SharedContextHolder.kt new file mode 100644 index 00000000..918ec1b5 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/SharedContextHolder.kt @@ -0,0 +1,15 @@ +package me.rhunk.snapenhance + +import android.content.Context +import java.lang.ref.WeakReference + +object SharedContextHolder { + private lateinit var _remoteSideContext: WeakReference + + fun remote(context: Context): RemoteSideContext { + if (!::_remoteSideContext.isInitialized || _remoteSideContext.get() == null) { + _remoteSideContext = WeakReference(RemoteSideContext(context.applicationContext)) + } + return _remoteSideContext.get()!! + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt similarity index 87% rename from core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt index 0a61a4e4..196ec6d9 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -3,7 +3,9 @@ package me.rhunk.snapenhance.bridge import android.app.Service import android.content.Intent import android.os.IBinder +import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.bridge.wrapper.MessageLoggerWrapper import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper @@ -11,7 +13,12 @@ import me.rhunk.snapenhance.download.DownloadProcessor class BridgeService : Service() { private lateinit var messageLoggerWrapper: MessageLoggerWrapper + private lateinit var remoteSideContext: RemoteSideContext + override fun onBind(intent: Intent): IBinder { + remoteSideContext = SharedContextHolder.remote(this).apply { + checkForRequirements() + } messageLoggerWrapper = MessageLoggerWrapper(getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)).also { it.init() } return BridgeBinder() } @@ -85,7 +92,7 @@ class BridgeService : Service() { 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 } @@ -98,6 +105,8 @@ class BridgeService : Service() { } override fun enqueueDownload(intent: Intent, callback: DownloadCallback) { + SharedContextHolder.remote(this@BridgeService) + //TODO: refactor shared context SharedContext.ensureInitialized(this@BridgeService) DownloadProcessor(this@BridgeService, callback).onReceive(intent) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt similarity index 97% rename from core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt index 9fc7038f..a9376e8c 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt @@ -59,10 +59,6 @@ class DownloadProcessor ( private val context: Context, private val callback: DownloadCallback ) { - companion object { - const val DOWNLOAD_REQUEST_EXTRA = "request" - const val DOWNLOAD_METADATA_EXTRA = "metadata" - } private val translation by lazy { SharedContext.translation.getCategory("download_processor") @@ -184,7 +180,9 @@ class DownloadProcessor ( fun handleInputStream(inputStream: InputStream) { createMediaTempFile().apply { if (inputMedia.encryption != null) { - decryptInputStream(inputStream, inputMedia.encryption).use { decryptedInputStream -> + decryptInputStream(inputStream, + inputMedia.encryption!! + ).use { decryptedInputStream -> decryptedInputStream.copyTo(outputStream()) } } else { @@ -286,8 +284,8 @@ class DownloadProcessor ( fun onReceive(intent: Intent) { CoroutineScope(Dispatchers.IO).launch { - val downloadMetadata = gson.fromJson(intent.getStringExtra(DOWNLOAD_METADATA_EXTRA)!!, DownloadMetadata::class.java) - val downloadRequest = gson.fromJson(intent.getStringExtra(DOWNLOAD_REQUEST_EXTRA)!!, DownloadRequest::class.java) + val downloadMetadata = gson.fromJson(intent.getStringExtra(DownloadManagerClient.DOWNLOAD_METADATA_EXTRA)!!, DownloadMetadata::class.java) + val downloadRequest = gson.fromJson(intent.getStringExtra(DownloadManagerClient.DOWNLOAD_REQUEST_EXTRA)!!, DownloadRequest::class.java) SharedContext.downloadTaskManager.canDownloadMedia(downloadMetadata.mediaIdentifier)?.let { downloadStage -> translation[if (downloadStage.isFinalStage) { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt index 7befc61a..51c9ed3c 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt @@ -1,43 +1,49 @@ package me.rhunk.snapenhance.ui.manager -import android.annotation.SuppressLint import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.runtime.remember import androidx.navigation.compose.rememberNavController +import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.ui.AppMaterialTheme -import me.rhunk.snapenhance.ui.manager.util.SaveFolderChecker -import me.rhunk.snapenhance.util.ActivityResultCallback class MainActivity : ComponentActivity() { - private val activityResultCallbacks = mutableMapOf() - - @SuppressLint("UnusedMaterialScaffoldPaddingParameter") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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 - SaveFolderChecker.askForFolder( - this, - managerContext.config.root.downloader.saveFolder) - { - managerContext.config.writeConfig() + val sections = EnumSection.values().toList().associateWith { + it.section.constructors.first().call() + }.onEach { (section, instance) -> + with(instance) { + enumSection = section + context = managerContext + init() + } } setContent { val navController = rememberNavController() - val navigation = Navigation(managerContext) + val navigation = remember { Navigation() } AppMaterialTheme { Scaffold( containerColor = MaterialTheme.colorScheme.background, bottomBar = { navigation.NavBar(navController = navController) } ) { innerPadding -> - navigation.NavigationHost(navController = navController, innerPadding = innerPadding, startDestination = startDestination) + navigation.NavigationHost( + sections = sections, + navController = navController, + innerPadding = innerPadding, + startDestination = startDestination + ) } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/ManagerContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/ManagerContext.kt deleted file mode 100644 index 42166d26..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/ManagerContext.kt +++ /dev/null @@ -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 - ) -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt index e6cbdce4..faf8d88d 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt @@ -12,7 +12,6 @@ import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavController @@ -23,25 +22,17 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.currentBackStackEntryAsState -class Navigation( - private val context: ManagerContext -) { +class Navigation{ @Composable fun NavigationHost( + sections: Map, startDestination: EnumSection, navController: NavHostController, 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)) { sections.forEach { (_, instance) -> + instance.navController = navController instance.build(this) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt index 064e51dd..ecef8d7f 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder 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.NotImplemented import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection @@ -61,9 +62,11 @@ enum class EnumSection( open class Section { lateinit var enumSection: EnumSection - lateinit var manager: ManagerContext + lateinit var context: RemoteSideContext lateinit var navController: NavController + open fun init() {} + @Composable open fun Content() { NotImplemented() } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/HomeSection.kt index e493a30c..34350152 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/HomeSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/HomeSection.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.manager.data.InstallationSummary +import me.rhunk.snapenhance.ui.setup.Requirements class HomeSection : Section() { companion object { @@ -76,7 +77,9 @@ class HomeSection : Section() { ) //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") } } @@ -102,7 +105,7 @@ class HomeSection : Section() { modifier = Modifier.padding(16.dp) ) - SummaryCards(manager.getInstallationSummary()) + SummaryCards(context.getInstallationSummary()) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt index 6958666c..c18a968e 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt @@ -140,14 +140,22 @@ class Dialogs { Text(text = "Cancel") } Button(onClick = { - if (property.key.dataType.type == DataProcessors.Type.INTEGER) { - runCatching { - property.value.setAny(fieldValue.value.text.toInt()) - }.onFailure { - property.value.setAny(0) + when (property.key.dataType.type) { + DataProcessors.Type.INTEGER -> { + runCatching { + property.value.setAny(fieldValue.value.text.toInt()) + }.onFailure { + property.value.setAny(0) + } } - } else { - property.value.setAny(fieldValue.value.text) + DataProcessors.Type.FLOAT -> { + runCatching { + property.value.setAny(fieldValue.value.text.toFloat()) + }.onFailure { + property.value.setAny(0f) + } + } + else -> property.value.setAny(fieldValue.value.text) } dismiss() }) { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt index e619dc78..9f500305 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt @@ -1,5 +1,6 @@ package me.rhunk.snapenhance.ui.manager.sections.features +import androidx.activity.ComponentActivity import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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.icons.Icons 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.rounded.Save 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.PropertyPair import me.rhunk.snapenhance.ui.manager.Section +import me.rhunk.snapenhance.ui.util.ChooseFolderHelper class FeaturesSection : Section() { private val dialogs by lazy { Dialogs() } @@ -63,6 +66,15 @@ class FeaturesSection : Section() { 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 private fun PropertyAction(property: PropertyPair<*>, registerClickCallback: RegisterClickCallback) { val showDialog = remember { mutableStateOf(false) } @@ -83,6 +95,18 @@ class FeaturesSection : Section() { 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 }) { DataProcessors.Type.BOOLEAN -> { val state = remember { mutableStateOf(propertyValue.get() as Boolean) } @@ -116,7 +140,7 @@ class FeaturesSection : Section() { DataProcessors.Type.STRING_MULTIPLE_SELECTION -> { dialogs.MultipleSelectionDialog(property) } - DataProcessors.Type.STRING, DataProcessors.Type.INTEGER -> { + DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> { dialogs.KeyboardInputDialog(property) { showDialog.value = false } } else -> {} @@ -124,7 +148,8 @@ class FeaturesSection : Section() { } 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) { Text( text = propertyValue.get().toString(), @@ -256,7 +281,7 @@ class FeaturesSection : Section() { floatingActionButton = { FloatingActionButton( onClick = { - manager.config.writeConfig() + context.config.writeConfig() scope.launch { scaffoldState.snackbarHostState.showSnackbar("Saved") } @@ -299,13 +324,13 @@ class FeaturesSection : Section() { } } } - queryContainerRecursive(manager.config.root) + queryContainerRecursive(context.config.root) containers } navGraphBuilder.navigation(route = "features", startDestination = MAIN_ROUTE) { composable(MAIN_ROUTE) { - Container(MAIN_ROUTE, manager.config.root) + Container(MAIN_ROUTE, context.config.root) } composable("container/{name}") { backStackEntry -> diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/util/SaveFolderChecker.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/util/SaveFolderChecker.kt deleted file mode 100644 index 952b1997..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/util/SaveFolderChecker.kt +++ /dev/null @@ -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, 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() - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt index 00121d12..8c7ca51a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt @@ -1,33 +1,21 @@ 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( - val firstRun: Boolean = false, - val language: Boolean = false, - val mappings: Boolean = false, - val saveFolder: Boolean = false, - val ffmpeg: Boolean = false -) { - companion object { - 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) - } + fun getName(requirement: Int): String { + return when (requirement) { + FIRST_RUN -> "FIRST_RUN" + LANGUAGE -> "LANGUAGE" + MAPPINGS -> "MAPPINGS" + SAVE_FOLDER -> "SAVE_FOLDER" + FFMPEG -> "FFMPEG" + else -> "UNKNOWN" } } } + diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt index 046d1392..a05bcdf7 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt @@ -1,7 +1,9 @@ package me.rhunk.snapenhance.ui.setup +import android.annotation.SuppressLint import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background @@ -14,6 +16,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowForwardIos +import androidx.compose.material.icons.filled.Check import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -28,48 +31,64 @@ import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.ui.AppMaterialTheme 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.LanguageScreen 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.WelcomeScreen class SetupActivity : ComponentActivity() { + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val requirements = intent.getBundleExtra("requirements")?.let { - Requirements.fromBundle(it) - } ?: Requirements(firstRun = true) + val setupContext = SharedContextHolder.remote(this).apply { + activity = this@SetupActivity + } + val requirements = intent.getIntExtra("requirements", Requirements.FIRST_RUN) + + fun hasRequirement(requirement: Int) = requirements and requirement == requirement val requiredScreens = mutableListOf() with(requiredScreens) { - with(requirements) { - if (firstRun || language) add(LanguageScreen().apply { route = "language" }) - if (firstRun) add(WelcomeScreen().apply { route = "welcome" }) - if (firstRun || saveFolder) add(SaveFolderScreen().apply { route = "saveFolder" }) - if (firstRun || mappings) add(MappingsScreen().apply { route = "mappings" }) - if (firstRun || ffmpeg) add(FfmpegScreen().apply { route = "ffmpeg" }) + val isFirstRun = hasRequirement(Requirements.FIRST_RUN) + if (isFirstRun || hasRequirement(Requirements.LANGUAGE)) { + add(PickLanguageScreen().apply { route = "language" }) + } + if (isFirstRun || hasRequirement(Requirements.SAVE_FOLDER)) { + 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()) { finish() return } + requiredScreens.forEach { screen -> + screen.context = setupContext + screen.init() + } + setContent { val navController = rememberNavController() val canGoNext = remember { mutableStateOf(false) } fun nextScreen() { if (!canGoNext.value) return - canGoNext.value = false if (requiredScreens.size > 1) { + canGoNext.value = false requiredScreens.removeFirst() navController.navigate(requiredScreens.first().route) } else { @@ -98,18 +117,21 @@ class SetupActivity : ComponentActivity() { .alpha(alpha) ) { Icon( - imageVector = Icons.Default.ArrowForwardIos, + imageVector = if (requiredScreens.size <= 1 && canGoNext.value) { + Icons.Default.Check + } else { + Icons.Default.ArrowForwardIos + }, contentDescription = null ) } } }, - ) { paddingValues -> + ) { Column( modifier = Modifier .background(MaterialTheme.colorScheme.background) .fillMaxSize() - .padding(paddingValues) ) { NavHost( navController = navController, @@ -118,6 +140,7 @@ class SetupActivity : ComponentActivity() { requiredScreens.forEach { screen -> screen.allowNext = { canGoNext.value = it } composable(screen.route) { + BackHandler(true) {} Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupContext.kt deleted file mode 100644 index 022761ba..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupContext.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/SetupScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/SetupScreen.kt index 1acdc3b1..5b0375a4 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/SetupScreen.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/SetupScreen.kt @@ -1,11 +1,31 @@ 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.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 { + lateinit var context: RemoteSideContext lateinit var allowNext: (Boolean) -> Unit 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 abstract fun Content() } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/LanguageScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/LanguageScreen.kt deleted file mode 100644 index e5eeaccd..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/LanguageScreen.kt +++ /dev/null @@ -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() { - - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt index c49494f2..b7e4cddc 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt @@ -1,16 +1,98 @@ 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.CircularProgressIndicator +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.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 class MappingsScreen : SetupScreen() { @Composable override fun Content() { - Text(text = "Mappings") - Button(onClick = { allowNext(true) }) { - Text(text = "Next") + val coroutineScope = rememberCoroutineScope() + val infoText = remember { mutableStateOf(null as String?) } + 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"]) + } } } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt new file mode 100644 index 00000000..dc5591d1 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt index 2d2884a3..aded8b86 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt @@ -1,17 +1,47 @@ 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.Text 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.util.ChooseFolderHelper class SaveFolderScreen : SetupScreen() { + private lateinit var saveFolder: ObservableMutableState + 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 override fun Content() { - Text(text = "SaveFolder") - Button(onClick = {allowNext(true)}) { - Text(text = "Next") + DialogText(text = context.translation["setup.dialogs.save_folder"]) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { + openFolderLauncher() + }) { + Text(text = context.translation["setup.dialogs.select_save_folder_button"]) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ChooseFolderHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ChooseFolderHelper.kt new file mode 100644 index 00000000..179591c9 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ChooseFolderHelper.kt @@ -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) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ObservableMutableState.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ObservableMutableState.kt new file mode 100644 index 00000000..6dc6af45 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ObservableMutableState.kt @@ -0,0 +1,19 @@ +package me.rhunk.snapenhance.ui.util + +import androidx.compose.runtime.MutableState + +class ObservableMutableState( + defaultValue: T, + inline val onChange: (T, T) -> Unit = { _, _ -> }, +) : MutableState { + 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 } +} \ No newline at end of file diff --git a/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl index 3e015a6a..846de0d2 100644 --- a/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl +++ b/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -83,11 +83,11 @@ interface BridgeInterface { void clearMessageLogger(); /** - * Fetch the translations + * Fetch the locales * - * @return the translations result + * @return the locale result */ - Map fetchTranslations(); + Map fetchLocales(String userLocale); /** * Get check for updates last time diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json index b3010e56..c4d7aa76 100644 --- a/core/src/main/assets/lang/en_US.json +++ b/core/src/main/assets/lang/en_US.json @@ -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": { "spying_privacy": "Spying & Privacy", "media_manager": "Media Manager", diff --git a/core/src/main/assets/lang/fr_FR.json b/core/src/main/assets/lang/fr_FR.json index e81f513d..a68882eb 100644 --- a/core/src/main/assets/lang/fr_FR.json +++ b/core/src/main/assets/lang/fr_FR.json @@ -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": { "spying_privacy": "Espionnage et vie privée", "media_manager": "Gestionnaire de média", diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/Logger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/Logger.kt index 9fcdf474..f4756fff 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/Logger.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/Logger.kt @@ -16,6 +16,11 @@ object Logger { Log.d(TAG, message.toString()) } + fun debug(tag: String, message: Any?) { + if (!BuildConfig.DEBUG) return + Log.d(tag, message.toString()) + } + fun error(throwable: Throwable) { Log.e(TAG, "", throwable) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt index d4c90334..b95247cd 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt @@ -122,4 +122,8 @@ class ModContext { fun reloadConfig() { modConfig.loadFromBridge(bridgeClient) } + + fun getConfigLocale(): String { + return modConfig.locale + } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt index a88578a2..20f01f6b 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -90,14 +90,14 @@ class SnapEnhance { @OptIn(ExperimentalTime::class) private suspend fun init() { - //load translations in a coroutine to speed up initialization - withContext(appContext.coroutineDispatcher) { - appContext.translation.loadFromBridge(appContext.bridgeClient) - } - measureTime { with(appContext) { reloadConfig() + withContext(appContext.coroutineDispatcher) { + translation.userLocale = getConfigLocale() + translation.loadFromBridge(appContext.bridgeClient) + } + mappings.init() eventDispatcher.init() //if mappings aren't loaded, we can't initialize features diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt index b3819ce3..e08b1bfa 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt @@ -36,8 +36,9 @@ class BridgeClient( .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) ) + //TODO: randomize package name 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) { bindService( intent, @@ -103,7 +104,7 @@ class BridgeClient( fun clearMessageLogger() = service.clearMessageLogger() - fun fetchTranslations() = service.fetchTranslations().map { + fun fetchLocales(userLocale: String) = service.fetchLocales(userLocale).map { LocalePair(it.key, it.value) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/LocaleWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/LocaleWrapper.kt index bc7968dd..28924760 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/LocaleWrapper.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/LocaleWrapper.kt @@ -13,15 +13,14 @@ class LocaleWrapper { companion object { const val DEFAULT_LOCALE = "en_US" - fun fetchLocales(context: Context): List { - val deviceLocale = Locale.getDefault().toString() - val locales = mutableListOf() + fun fetchLocales(context: Context, locale: String = DEFAULT_LOCALE): List { + val locales = mutableListOf().apply { + 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(deviceLocale) }?.substring(0, 5) ?: return locales + val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(locale) }?.substring(0, 5) ?: return locales context.resources.assets.open("lang/$compatibleLocale.json").use { inputStream -> locales.add(LocalePair(compatibleLocale, inputStream.bufferedReader().use { it.readText() })) @@ -29,19 +28,24 @@ class LocaleWrapper { return locales } + + fun fetchAvailableLocales(context: Context): List { + return context.resources.assets.list("lang")?.map { it.substring(0, 5) } ?: listOf() + } } + var userLocale = DEFAULT_LOCALE private val translationMap = linkedMapOf() - private lateinit var _locale: String + private lateinit var _loadedLocaleString: String - val locale by lazy { - Locale(_locale.substring(0, 2), _locale.substring(3, 5)) + val loadedLocale by lazy { + Locale(_loadedLocaleString.substring(0, 2), _loadedLocaleString.substring(3, 5)) } private fun load(localePair: LocalePair) { - if (!::_locale.isInitialized) { - _locale = localePair.locale + if (!::_loadedLocaleString.isInitialized) { + _loadedLocaleString = localePair.locale } val translations = JsonParser.parseString(localePair.content).asJsonObject @@ -64,17 +68,23 @@ class LocaleWrapper { } fun loadFromBridge(bridgeClient: BridgeClient) { - bridgeClient.fetchTranslations().forEach { + bridgeClient.fetchLocales(userLocale).forEach { load(it) } } fun loadFromContext(context: Context) { - fetchLocales(context).forEach { + fetchLocales(context, userLocale).forEach { load(it) } } + fun reloadFromContext(context: Context, locale: String) { + userLocale = locale + translationMap.clear() + loadFromContext(context) + } + operator fun get(key: String): String { return translationMap[key] ?: key.also { Logger.debug("Missing translation for $key") } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MappingsWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MappingsWrapper.kt index ecf08d1f..f410513a 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MappingsWrapper.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MappingsWrapper.kt @@ -46,7 +46,6 @@ class MappingsWrapper( private val mappings = ConcurrentHashMap() private var snapBuildNumber: Long = 0 - @Suppress("deprecation") fun init() { snapBuildNumber = getSnapchatVersionCode() @@ -54,6 +53,7 @@ class MappingsWrapper( runCatching { loadCached() }.onFailure { + Logger.error("Failed to load cached mappings", it) delete() } } @@ -100,6 +100,7 @@ class MappingsWrapper( } fun refresh() { + snapBuildNumber = getSnapchatVersionCode() val mapper = Mapper(*mappers) runCatching { @@ -114,7 +115,7 @@ class MappingsWrapper( } write(result.toString().toByteArray()) }.also { - Logger.xposedLog("Generated mappings in $it ms") + Logger.debug("Generated mappings in $it ms") } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt index bba5bbfa..0b968390 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt @@ -38,7 +38,7 @@ open class ConfigContainer( vararg values: String = emptyArray(), params: ConfigParamsBuilder = {} ) = registerProperty(key, - DataProcessors.STRING_MULTIPLE_SELECTION, PropertyValue(emptyList(), defaultValues = values.toList()), params) + DataProcessors.STRING_MULTIPLE_SELECTION, PropertyValue(mutableListOf(), defaultValues = values.toList()), params) //null value is considered as Off/Disabled protected fun unique( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ModConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ModConfig.kt index 8d4890a0..11e7bfb9 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ModConfig.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ModConfig.kt @@ -10,22 +10,20 @@ import me.rhunk.snapenhance.bridge.FileLoaderWrapper import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.core.config.impl.RootConfig +import kotlin.properties.Delegates class ModConfig { - var locale: String = LocaleWrapper.DEFAULT_LOCALE - set(value) { - field = value - writeConfig() - } private val gson: Gson = GsonBuilder().setPrettyPrinting().create() private val file = FileLoaderWrapper(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8)) + var wasPresent by Delegates.notNull() val root = RootConfig() operator fun getValue(thisRef: Any?, property: Any?) = root private fun load() { + wasPresent = file.isFileExists() if (!file.isFileExists()) { writeConfig() return @@ -42,12 +40,13 @@ class ModConfig { private fun loadConfig() { val configFileContent = file.read() 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() { val configObject = root.toJson() - configObject.addProperty("language", locale) + configObject.addProperty("_locale", locale) file.write(configObject.toString().toByteArray(Charsets.UTF_8)) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt index d973b2f4..7256a5a2 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt @@ -16,11 +16,16 @@ class DownloadManagerClient ( private val metadata: DownloadMetadata, private val callback: DownloadCallback ) { + companion object { + const val DOWNLOAD_REQUEST_EXTRA = "request" + const val DOWNLOAD_METADATA_EXTRA = "metadata" + } + private fun enqueueDownloadRequest(request: DownloadRequest) { context.bridgeClient.enqueueDownload(Intent().apply { putExtras(Bundle().apply { - putString(DownloadProcessor.DOWNLOAD_REQUEST_EXTRA, context.gson.toJson(request)) - putString(DownloadProcessor.DOWNLOAD_METADATA_EXTRA, context.gson.toJson(metadata)) + putString(DOWNLOAD_REQUEST_EXTRA, context.gson.toJson(request)) + putString(DOWNLOAD_METADATA_EXTRA, context.gson.toJson(metadata)) }) }, callback) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/FeatureLoadParams.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/FeatureLoadParams.kt index fbbbc2f4..46e201e6 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/features/FeatureLoadParams.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/features/FeatureLoadParams.kt @@ -3,9 +3,9 @@ package me.rhunk.snapenhance.features object FeatureLoadParams { const val NO_INIT = 0 - const val INIT_SYNC = 1 - const val ACTIVITY_CREATE_SYNC = 2 + const val INIT_SYNC = 0b0001 + const val ACTIVITY_CREATE_SYNC = 0b0010 - const val INIT_ASYNC = 3 - const val ACTIVITY_CREATE_ASYNC = 4 + const val INIT_ASYNC = 0b0100 + const val ACTIVITY_CREATE_ASYNC = 0b1000 } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt index 3d2c3f01..2c1156bf 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt @@ -84,7 +84,7 @@ class FriendFeedInfoMenu : AbstractMenu() { ${birthday.getDisplayName( Calendar.MONTH, Calendar.LONG, - context.translation.locale + context.translation.loadedLocale )?.let { context.translation.format("profile_info.birthday", "month" to it, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsGearInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsGearInjector.kt index a1887caf..0a68d103 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsGearInjector.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsGearInjector.kt @@ -47,9 +47,8 @@ class SettingsGearInjector : AbstractMenu() { setOnClickListener { 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("lspatched", File(context.cacheDir, "lspatch/origin").exists()) } context.startActivity(intent) }