feat(export): add saved locations export with toggle

This commit is contained in:
daplugg23
2026-02-05 14:54:32 -06:00
parent 67dcc16923
commit 5c7b342195
35 changed files with 5351 additions and 6133 deletions

View File

@@ -54,7 +54,7 @@ class Routes(
) {
companion object {
const val CONFIG_IMPORT_CONFIRMATION_ROUTE = "config_import_confirmation"
const val CONFIG_EXPORT_SUMMARY_ROUTE = "config_export_summary/?exportSensitiveData={exportSensitiveData}"
const val CONFIG_EXPORT_SUMMARY_ROUTE = "config_export_summary/?exportSensitiveData={exportSensitiveData}&includeSavedLocations={includeSavedLocations}"
const val FRIEND_TRACKER_CONFIG_EXPORT_ROUTE = "friend_tracker_config_export/?rule_id={rule_id}"
const val FRIEND_TRACKER_CONFIG_IMPORT_ROUTE = "friend_tracker_config_import"
const val VIEW_LOGGER_HISTORY_ROUTE = "view_logger_history/{uri}"

View File

@@ -52,6 +52,7 @@ import androidx.compose.ui.unit.sp
import me.eternal.purrfectsnap.ui.manager.Routes
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
import me.eternal.purrfectsnap.ui.util.saveFile
import me.eternal.purrfectsnap.storage.getLocationCoordinates
import org.json.JSONArray
import org.json.JSONObject
@@ -136,10 +137,14 @@ class ConfigExportSummaryScreen : Routes.Route() {
override val content: @Composable (androidx.navigation.NavBackStackEntry) -> Unit = {
val exportSensitiveData = it.arguments?.getString("exportSensitiveData")?.toBoolean() ?: false
val includeSavedLocations = it.arguments?.getString("includeSavedLocations")?.toBoolean() ?: false
val exportLabel = context.translation["manager.sections.features.export_option"] ?: "Export"
val parser = remember { ConfigParser() }
val savedLocations = remember {
if (includeSavedLocations) context.database.getLocationCoordinates() else null
}
val featuresByCategory = remember {
parser.parse(context.config.exportToString(exportSensitiveData))
parser.parse(context.config.exportToString(exportSensitiveData, includeSavedLocations, savedLocations))
}
val expandedState = remember { mutableStateMapOf<String, Boolean>() }
@@ -210,7 +215,7 @@ class ConfigExportSummaryScreen : Routes.Route() {
runCatching {
context.androidContext.contentResolver.openOutputStream(android.net.Uri.parse(uri))?.use {
context.config.writeConfig()
context.config.exportToString(exportSensitiveData).byteInputStream().copyTo(it)
context.config.exportToString(exportSensitiveData, includeSavedLocations, savedLocations).byteInputStream().copyTo(it)
context.shortToast(context.translation["manager.sections.features.config_export_success_toast"])
}
}.onFailure {

View File

@@ -49,10 +49,14 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.eternal.purrfectsnap.bridge.location.LocationCoordinates
import me.eternal.purrfectsnap.storage.addOrUpdateLocationCoordinate
import me.eternal.purrfectsnap.storage.getLocationCoordinates
import me.eternal.purrfectsnap.ui.manager.Routes
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
import org.json.JSONArray
import org.json.JSONObject
import kotlin.math.abs
class ConfigImportConfirmationScreen : Routes.Route() {
override val translation by lazy { context.translation.getCategory("manager.features.config_import") }
@@ -65,6 +69,44 @@ class ConfigImportConfirmationScreen : Routes.Route() {
val indentation: Int
)
companion object {
private const val COORDINATE_TOLERANCE = 0.0001 // ~11 meters tolerance for de-duplication
}
/**
* Imports saved locations from JSON array into database with de-duplication.
* Only adds locations that don't already exist (within coordinate tolerance).
*/
private fun importSavedLocations(locationsArray: com.google.gson.JsonArray) {
val existingLocations = context.database.getLocationCoordinates()
for (i in 0 until locationsArray.size()) {
val locationObj = locationsArray.get(i).asJsonObject
val name = locationObj.get("name")?.asString ?: continue
val latitude = locationObj.get("latitude")?.asDouble ?: continue
val longitude = locationObj.get("longitude")?.asDouble ?: continue
val radius = locationObj.get("radius")?.asDouble ?: 100.0
// Check for existing location with similar coordinates (de-duplication)
val existingMatch = existingLocations.find { existing ->
abs(existing.latitude - latitude) < COORDINATE_TOLERANCE &&
abs(existing.longitude - longitude) < COORDINATE_TOLERANCE
}
if (existingMatch == null) {
// No duplicate found, add as new location
val newLocation = LocationCoordinates().apply {
this.name = name
this.latitude = latitude
this.longitude = longitude
this.radius = radius
}
context.database.addOrUpdateLocationCoordinate(null, newLocation)
}
// If duplicate exists, skip (do not update or delete existing)
}
}
private inner class ConfigParser {
fun parse(configJson: String): Map<String, List<ImportedFeature>> {
val featureList = mutableListOf<ImportedFeature>()
@@ -256,7 +298,12 @@ class ConfigImportConfirmationScreen : Routes.Route() {
onClick = {
routes.configJsonForImport?.let { json ->
runCatching {
context.config.loadFromString(json)
val savedLocationsJson = context.config.loadFromString(json)
// Import saved locations if present in the JSON
savedLocationsJson?.let { locationsArray ->
importSavedLocations(locationsArray)
}
}.onFailure { err ->
context.longToast(
context.translation.format(

View File

@@ -1060,9 +1060,11 @@ class FeaturesRootSection : Routes.Route() {
@Composable
private fun SensitiveDataDialog(
onDismiss: () -> Unit,
onConfirm: (exportSensitiveData: Boolean) -> Unit
onConfirm: (exportSensitiveData: Boolean, includeSavedLocations: Boolean) -> Unit
) {
Dialog(onDismissRequest = onDismiss) {
val includeSavedLocations = remember { mutableStateOf(false) }
Surface(
shape = RoundedCornerShape(24.dp),
color = Color.White.copy(alpha = 0.06f),
@@ -1100,12 +1102,36 @@ class FeaturesRootSection : Routes.Route() {
color = PurrfectPalette.textSecondary,
modifier = Modifier.padding(horizontal = 6.dp)
)
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Include Saved Locations",
style = MaterialTheme.typography.bodyMedium,
color = Color.White
)
val hapticFeedback = LocalHapticFeedback.current
Switch(
checked = includeSavedLocations.value,
onCheckedChange = {
if (context.config.root.global.uiSettings.hapticFeedback.get()) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
includeSavedLocations.value = it
},
colors = purrfectSwitchColors()
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End)
) {
Button(
onClick = { onConfirm(false) },
onClick = { onConfirm(false, includeSavedLocations.value) },
colors = ButtonDefaults.buttonColors(
containerColor = Color.White.copy(alpha = 0.08f),
contentColor = Color.White
@@ -1114,7 +1140,7 @@ class FeaturesRootSection : Routes.Route() {
Text(context.translation["button.negative"])
}
Button(
onClick = { onConfirm(true) },
onClick = { onConfirm(true, includeSavedLocations.value) },
colors = ButtonDefaults.buttonColors(
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.32f),
contentColor = Color.White
@@ -1244,10 +1270,11 @@ class FeaturesRootSection : Routes.Route() {
if (showExportDialog) {
SensitiveDataDialog(
onDismiss = { showExportDialog = false },
onConfirm = { exportSensitiveData ->
onConfirm = { exportSensitiveData, includeSavedLocations ->
showExportDialog = false
routes.configExportSummary.navigate {
put("exportSensitiveData", exportSensitiveData.toString())
put("includeSavedLocations", includeSavedLocations.toString())
}
}
)