mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-28 04:20:20 +02:00
feat(ui): setup activity
- remote side context - fix float dialogs - fix choose folder
This commit is contained in:
parent
853ceec290
commit
641d66b208
@ -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")
|
||||
|
@ -50,8 +50,7 @@
|
||||
android:name=".ui.setup.SetupActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:excludeFromRecents="true">
|
||||
</activity>
|
||||
android:excludeFromRecents="true" />
|
||||
<activity
|
||||
android:name=".ui.map.MapActivity"
|
||||
android:exported="true"
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()!!
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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) {
|
@ -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<Int, ActivityResultCallback>()
|
||||
|
||||
@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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -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<EnumSection, Section>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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() }
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}) {
|
||||
|
@ -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 ->
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<SetupScreen>()
|
||||
|
||||
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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
@ -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"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<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
|
||||
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"])
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
@ -83,11 +83,11 @@ interface BridgeInterface {
|
||||
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
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -122,4 +122,8 @@ class ModContext {
|
||||
fun reloadConfig() {
|
||||
modConfig.loadFromBridge(bridgeClient)
|
||||
}
|
||||
|
||||
fun getConfigLocale(): String {
|
||||
return modConfig.locale
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -13,15 +13,14 @@ class LocaleWrapper {
|
||||
companion object {
|
||||
const val DEFAULT_LOCALE = "en_US"
|
||||
|
||||
fun fetchLocales(context: Context): List<LocalePair> {
|
||||
val deviceLocale = Locale.getDefault().toString()
|
||||
val locales = mutableListOf<LocalePair>()
|
||||
fun fetchLocales(context: Context, locale: String = DEFAULT_LOCALE): List<LocalePair> {
|
||||
val locales = mutableListOf<LocalePair>().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<String> {
|
||||
return context.resources.assets.list("lang")?.map { it.substring(0, 5) } ?: listOf()
|
||||
}
|
||||
}
|
||||
|
||||
var userLocale = DEFAULT_LOCALE
|
||||
|
||||
private val translationMap = linkedMapOf<String, String>()
|
||||
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") }
|
||||
}
|
||||
|
@ -46,7 +46,6 @@ class MappingsWrapper(
|
||||
private val mappings = ConcurrentHashMap<String, Any>()
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ open class ConfigContainer(
|
||||
vararg values: String = emptyArray(),
|
||||
params: ConfigParamsBuilder = {}
|
||||
) = 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
|
||||
protected fun unique(
|
||||
|
@ -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<Boolean>()
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user