chore(translation): new strings

This commit is contained in:
rhunk 2024-04-06 15:34:18 +02:00
parent b610825a22
commit 1d0456e8a0
13 changed files with 238 additions and 154 deletions

View File

@ -78,6 +78,8 @@ class Routes(
lateinit var routeInfo: RouteInfo lateinit var routeInfo: RouteInfo
lateinit var routes: Routes lateinit var routes: Routes
val translation by lazy { context.translation.getCategory("manager.sections.${routeInfo.key.substringBefore("/")}")}
private fun replaceArguments(id: String, args: Map<String, String>) = args.takeIf { it.isNotEmpty() }?.let { private fun replaceArguments(id: String, args: Map<String, String>) = args.takeIf { it.isNotEmpty() }?.let {
args.entries.fold(id) { acc, (key, value) -> args.entries.fold(id) { acc, (key, value) ->
acc.replace("{$key}", value) acc.replace("{$key}", value)

View File

@ -126,7 +126,7 @@ class LoggerHistoryRoot : Routes.Route() {
LaunchedEffect(Unit, message) { LaunchedEffect(Unit, message) {
runCatching { runCatching {
decodeMessage(message) { senderId, contentType, messageReader, attachments -> decodeMessage(message) { senderId, contentType, messageReader, attachments ->
val senderUsername = senderId?.let { context.modDatabase.getFriendInfo(it)?.mutableUsername } ?: "unknown sender" val senderUsername = senderId?.let { context.modDatabase.getFriendInfo(it)?.mutableUsername } ?: translation["unknown_sender"]
@Composable @Composable
fun ContentHeader() { fun ContentHeader() {
@ -134,7 +134,7 @@ class LoggerHistoryRoot : Routes.Route() {
} }
if (contentType == ContentType.CHAT) { if (contentType == ContentType.CHAT) {
val content = messageReader.getString(2, 1) ?: "[empty chat message]" val content = messageReader.getString(2, 1) ?: "[${translation["empty_message"]}]"
contentView = { contentView = {
Column { Column {
Text(content, modifier = Modifier Text(content, modifier = Modifier
@ -166,7 +166,7 @@ class LoggerHistoryRoot : Routes.Route() {
downloadAttachment(message.timestamp, attachment) downloadAttachment(message.timestamp, attachment)
}.onFailure { }.onFailure {
context.log.error("Failed to download attachment", it) context.log.error("Failed to download attachment", it)
context.shortToast("Failed to download attachment") context.shortToast(translation["download_attachment_failed_toast"])
} }
} }
}) { }) {
@ -175,7 +175,7 @@ class LoggerHistoryRoot : Routes.Route() {
contentDescription = "Download", contentDescription = "Download",
modifier = Modifier.padding(end = 4.dp) modifier = Modifier.padding(end = 4.dp)
) )
Text("Attachment ${index + 1}") Text(translation.format("chat_attachment", "index" to (index + 1).toString()))
} }
} }
} }
@ -186,7 +186,7 @@ class LoggerHistoryRoot : Routes.Route() {
}.onFailure { }.onFailure {
context.log.error("Failed to parse message", it) context.log.error("Failed to parse message", it)
contentView = { contentView = {
Text("[Failed to parse message]") Text("[${translation["message_parse_failed"]}]")
} }
} }
} }
@ -212,8 +212,10 @@ class LoggerHistoryRoot : Routes.Route() {
) { ) {
fun formatConversationId(conversationId: String?): String? { fun formatConversationId(conversationId: String?): String? {
if (conversationId == null) return null if (conversationId == null) return null
return context.modDatabase.getGroupInfo(conversationId)?.name?.let { "Group $it" } ?: context.modDatabase.findFriend(conversationId)?.let { return context.modDatabase.getGroupInfo(conversationId)?.name?.let {
"Friend " + (it.displayName?.let { name -> "$name (${it.mutableUsername})" } ?: it.mutableUsername) translation.format("list_group_format", "name" to it)
} ?: context.modDatabase.findFriend(conversationId)?.let {
translation.format("list_friend_format", "name" to (it.displayName?.let { name -> "$name (${it.mutableUsername})" } ?: it.mutableUsername))
} ?: conversationId } ?: conversationId
} }
@ -257,7 +259,7 @@ class LoggerHistoryRoot : Routes.Route() {
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp), horizontalArrangement = Arrangement.spacedBy(2.dp),
) { ) {
Text("Reverse order") Text(translation["reverse_order_checkbox"])
Checkbox(checked = reverseOrder, onCheckedChange = { Checkbox(checked = reverseOrder, onCheckedChange = {
reverseOrder = it reverseOrder = it
}) })
@ -275,7 +277,7 @@ class LoggerHistoryRoot : Routes.Route() {
item { item {
if (selectedConversation != null) { if (selectedConversation != null) {
if (hasReachedEnd) { if (hasReachedEnd) {
Text("No more messages", modifier = Modifier Text(translation["no_more_messages"], modifier = Modifier
.padding(8.dp) .padding(8.dp)
.fillMaxWidth(), textAlign = TextAlign.Center) .fillMaxWidth(), textAlign = TextAlign.Center)
} else { } else {

View File

@ -99,7 +99,7 @@ class TasksRoot : Routes.Route() {
} }
runCatching { runCatching {
context.shortToast("Merging ${filesToMerge.size} files") context.shortToast(translation.format("merge_files_toast", "count" to filesToMerge.size.toString()))
FFMpegProcessor.newFFMpegProcessor(context, pendingTask).execute( FFMpegProcessor.newFFMpegProcessor(context, pendingTask).execute(
FFMpegProcessor.Request(FFMpegProcessor.Action.MERGE_MEDIA, filesToMerge.map { it.absolutePath }, mergedFile) FFMpegProcessor.Request(FFMpegProcessor.Action.MERGE_MEDIA, filesToMerge.map { it.absolutePath }, mergedFile)
) )
@ -177,15 +177,15 @@ class TasksRoot : Routes.Route() {
onDismissRequest = { showConfirmDialog = false }, onDismissRequest = { showConfirmDialog = false },
title = { title = {
if (taskSelection.isNotEmpty()) { if (taskSelection.isNotEmpty()) {
Text("Remove ${taskSelection.size} tasks?") Text(translation.format("remove_selected_tasks_confirm", "count" to taskSelection.size.toString()))
} else { } else {
Text("Remove all tasks?") Text(translation["remove_all_tasks_confirm"])
} }
}, },
text = { text = {
Column { Column {
if (taskSelection.isNotEmpty()) { if (taskSelection.isNotEmpty()) {
Text("Are you sure you want to remove selected tasks?") Text(translation["remove_selected_tasks_title"])
Row ( Row (
modifier = Modifier.padding(top = 10.dp).fillMaxWidth().clickable { modifier = Modifier.padding(top = 10.dp).fillMaxWidth().clickable {
alsoDeleteFiles = !alsoDeleteFiles alsoDeleteFiles = !alsoDeleteFiles
@ -196,10 +196,10 @@ class TasksRoot : Routes.Route() {
Checkbox(checked = alsoDeleteFiles, onCheckedChange = { Checkbox(checked = alsoDeleteFiles, onCheckedChange = {
alsoDeleteFiles = it alsoDeleteFiles = it
}) })
Text("Also delete files") Text(translation["delete_files_option"])
} }
} else { } else {
Text("Are you sure you want to remove all tasks?") Text(translation["remove_all_tasks_title"])
} }
} }
}, },
@ -233,7 +233,7 @@ class TasksRoot : Routes.Route() {
} }
} }
) { ) {
Text("Yes") Text(context.translation["button.positive"])
} }
}, },
dismissButton = { dismissButton = {
@ -242,7 +242,7 @@ class TasksRoot : Routes.Route() {
showConfirmDialog = false showConfirmDialog = false
} }
) { ) {
Text("No") Text(context.translation["button.negative"])
} }
} }
) )
@ -429,7 +429,7 @@ class TasksRoot : Routes.Route() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
context.translation["manager.sections.tasks.no_tasks"].let { translation["no_tasks"].let {
Icon(Icons.Filled.CheckCircle, contentDescription = it, tint = MaterialTheme.colorScheme.primary) Icon(Icons.Filled.CheckCircle, contentDescription = it, tint = MaterialTheme.colorScheme.primary)
Text(it, style = MaterialTheme.typography.bodyLarge) Text(it, style = MaterialTheme.typography.bodyLarge)
} }

View File

@ -17,7 +17,6 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.rounded.Save
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -30,11 +29,13 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -53,7 +54,6 @@ class FeaturesRoot : Routes.Route() {
} }
private var activityLauncherHelper: ActivityLauncherHelper? = null private var activityLauncherHelper: ActivityLauncherHelper? = null
private lateinit var rememberScaffoldState: BottomSheetScaffoldState
private val allContainers by lazy { private val allContainers by lazy {
val containers = mutableMapOf<String, PropertyPair<*>>() val containers = mutableMapOf<String, PropertyPair<*>>()
@ -444,48 +444,62 @@ class FeaturesRoot : Routes.Route() {
var showResetConfirmationDialog by remember { mutableStateOf(false) } var showResetConfirmationDialog by remember { mutableStateOf(false) }
if (showResetConfirmationDialog) { if (showResetConfirmationDialog) {
Dialog(onDismissRequest = { showResetConfirmationDialog = false }) { AlertDialog(
alertDialogs.ConfirmDialog( title = { Text(text = context.translation["manager.dialogs.reset_config.title"]) },
title = "Reset config", text = { Text(text = context.translation["manager.dialogs.reset_config.content"]) },
message = "Are you sure you want to reset the config?", onDismissRequest = { showResetConfirmationDialog = false },
onConfirm = { confirmButton = {
Button(
onClick = {
context.config.reset() context.config.reset()
context.shortToast("Config successfully reset!") context.shortToast(context.translation["manager.dialogs.reset_config.success_toast"])
}, showResetConfirmationDialog = false
onDismiss = { showResetConfirmationDialog = false }
)
} }
) {
Text(text = context.translation["button.positive"])
}
},
dismissButton = {
Button(
onClick = {
showResetConfirmationDialog = false
}
) {
Text(text = context.translation["button.negative"])
}
}
)
} }
val actions = remember { val actions = remember {
mapOf( mapOf(
"Export" to { translation["export_option"] to {
activityLauncher { activityLauncher {
saveFile("config.json", "application/json") { uri -> saveFile("config.json", "application/json") { uri ->
context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use { context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use {
context.config.writeConfig() context.config.writeConfig()
context.config.exportToString().byteInputStream().copyTo(it) context.config.exportToString().byteInputStream().copyTo(it)
context.shortToast("Config exported successfully!") context.shortToast(translation["config_export_success_toast"])
} }
} }
} }
}, },
"Import" to { translation["import_option"] to {
activityLauncher { activityLauncher {
openFile("application/json") { uri -> openFile("application/json") { uri ->
context.androidContext.contentResolver.openInputStream(Uri.parse(uri))?.use { context.androidContext.contentResolver.openInputStream(Uri.parse(uri))?.use {
runCatching { runCatching {
context.config.loadFromString(it.readBytes().toString(Charsets.UTF_8)) context.config.loadFromString(it.readBytes().toString(Charsets.UTF_8))
}.onFailure { }.onFailure {
context.longToast("Failed to import config ${it.message}") context.longToast(translation.format("config_import_failure_toast", "error" to it.message.toString()))
return@use return@use
} }
context.shortToast("Config successfully loaded!") context.shortToast(translation["config_import_success_toast"])
} }
} }
} }
}, },
"Reset" to { showResetConfirmationDialog = true } translation["reset_option"] to { showResetConfirmationDialog = true }
) )
} }
@ -519,9 +533,7 @@ class FeaturesRoot : Routes.Route() {
private fun PropertiesView( private fun PropertiesView(
properties: List<PropertyPair<*>> properties: List<PropertyPair<*>>
) { ) {
rememberScaffoldState = rememberBottomSheetScaffoldState()
Scaffold( Scaffold(
snackbarHost = { SnackbarHost(rememberScaffoldState.snackbarHostState) },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
content = { innerPadding -> content = { innerPadding ->
LazyColumn( LazyColumn(
@ -541,23 +553,23 @@ class FeaturesRoot : Routes.Route() {
} }
override val floatingActionButton: @Composable () -> Unit = { override val floatingActionButton: @Composable () -> Unit = {
val scope = rememberCoroutineScope() fun saveConfig() {
FloatingActionButton( context.coroutineScope.launch(Dispatchers.IO) {
onClick = {
context.config.writeConfig() context.config.writeConfig()
scope.launch { context.log.verbose("saved config!")
rememberScaffoldState.snackbarHostState.showSnackbar("Saved") }
}
OnLifecycleEvent { _, event ->
if (event == Lifecycle.Event.ON_PAUSE || event == Lifecycle.Event.ON_STOP) {
saveConfig()
}
}
DisposableEffect(Unit) {
onDispose {
saveConfig()
} }
},
modifier = Modifier.padding(10.dp),
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
shape = RoundedCornerShape(16.dp),
) {
Icon(
imageVector = Icons.Rounded.Save,
contentDescription = null
)
} }
} }
@ -566,10 +578,8 @@ class FeaturesRoot : Routes.Route() {
private fun Container( private fun Container(
configContainer: ConfigContainer configContainer: ConfigContainer
) { ) {
val properties = remember { PropertiesView(remember {
configContainer.properties.map { PropertyPair(it.key, it.value) } configContainer.properties.map { PropertyPair(it.key, it.value) }
} })
PropertiesView(properties)
} }
} }

View File

@ -68,28 +68,27 @@ class HomeLogs : Routes.Route() {
navigate() navigate()
showDropDown = false showDropDown = false
}, text = { }, text = {
Text( Text(translation["clear_logs_button"])
text = context.translation["manager.sections.home.logs.clear_logs_button"]
)
}) })
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
activityLauncherHelper.saveFile("snapenhance-logs-${System.currentTimeMillis()}.zip", "application/zip") { uri -> activityLauncherHelper.saveFile("snapenhance-logs-${System.currentTimeMillis()}.zip", "application/zip") { uri ->
context.coroutineScope.launch {
context.shortToast(translation["saving_logs_toast"])
context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use { context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use {
runCatching { runCatching {
context.log.exportLogsToZip(it) context.log.exportLogsToZip(it)
context.longToast("Saved logs to $uri") context.longToast(translation["saved_logs_success_toast"])
}.onFailure { }.onFailure {
context.longToast("Failed to save logs to $uri!") context.longToast(translation["saved_logs_failure_toast"])
context.log.error("Failed to save logs to $uri!", it) context.log.error("Failed to save logs to $uri!", it)
} }
} }
} }
}
showDropDown = false showDropDown = false
}, text = { }, text = {
Text( Text(translation["export_logs_button"])
text = context.translation["manager.sections.home.logs.export_logs_button"]
)
}) })
} }
} }
@ -141,7 +140,7 @@ class HomeLogs : Routes.Route() {
item { item {
if (lineCount == 0 && logReader != null) { if (lineCount == 0 && logReader != null) {
Text( Text(
text = "No logs found!", text = translation["no_logs_hint"],
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
fontSize = 12.sp, fontSize = 12.sp,
fontWeight = FontWeight.Light fontWeight = FontWeight.Light

View File

@ -198,9 +198,9 @@ class HomeRoot : Routes.Route() {
} }
) )
} }
Spacer(modifier = Modifier.height(20.dp))
if (latestUpdate != null) { if (latestUpdate != null) {
Spacer(modifier = Modifier.height(20.dp))
OutlinedCard( OutlinedCard(
modifier = Modifier modifier = Modifier
.padding(all = cardMargin) .padding(all = cardMargin)
@ -218,13 +218,13 @@ class HomeRoot : Routes.Route() {
) { ) {
Column { Column {
Text( Text(
text = "SnapEnhance Update", text = translation["update_title"],
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
Text( Text(
fontSize = 12.sp, fontSize = 12.sp,
text = "Version ${latestUpdate?.versionName} is available!", text = translation.format("update_content", "version" to (latestUpdate?.versionName ?: "unknown")),
lineHeight = 20.sp lineHeight = 20.sp
) )
} }
@ -235,7 +235,7 @@ class HomeRoot : Routes.Route() {
} }
) )
}, modifier = Modifier.height(40.dp)) { }, modifier = Modifier.height(40.dp)) {
Text(text = "Download") Text(text = translation["update_button"])
} }
} }
} }

View File

@ -84,7 +84,7 @@ class HomeSettings : Routes.Route() {
if (requireConfirmation && confirmationDialog) { if (requireConfirmation && confirmationDialog) {
Dialog(onDismissRequest = { confirmationDialog = false }) { Dialog(onDismissRequest = { confirmationDialog = false }) {
dialogs.ConfirmDialog(title = "Are you sure?", onConfirm = { dialogs.ConfirmDialog(title = context.translation["manager.dialogs.action_confirm.title"], onConfirm = {
action() action()
confirmationDialog = false confirmationDialog = false
}, onDismiss = { }, onDismiss = {
@ -148,7 +148,7 @@ class HomeSettings : Routes.Route() {
.fillMaxSize() .fillMaxSize()
.verticalScroll(ScrollState(0)) .verticalScroll(ScrollState(0))
) { ) {
RowTitle(title = "Actions") RowTitle(title = translation["actions_title"])
EnumAction.entries.forEach { enumAction -> EnumAction.entries.forEach { enumAction ->
RowAction(key = enumAction.key) { RowAction(key = enumAction.key) {
launchActionIntent(enumAction) launchActionIntent(enumAction)
@ -160,7 +160,7 @@ class HomeSettings : Routes.Route() {
RowAction(key = "change_language") { RowAction(key = "change_language") {
context.checkForRequirements(Requirements.LANGUAGE) context.checkForRequirements(Requirements.LANGUAGE)
} }
RowTitle(title = "Message Logger") RowTitle(title = translation["message_logger_title"])
ShiftedRow { ShiftedRow {
Column( Column(
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp),
@ -184,8 +184,11 @@ class HomeSettings : Routes.Route() {
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
) { ) {
Text(text = "$storedMessagesCount messages") Text(
Text(text = "$storedStoriesCount stories") translation.format("message_logger_summary",
"messageCount" to storedMessagesCount.toString(),
"storyCount" to storedStoriesCount.toString()
), maxLines = 2)
} }
Button(onClick = { Button(onClick = {
runCatching { runCatching {
@ -201,7 +204,7 @@ class HomeSettings : Routes.Route() {
context.longToast("Failed to export database! ${it.localizedMessage}") context.longToast("Failed to export database! ${it.localizedMessage}")
} }
}) { }) {
Text(text = "Export") Text(text = translation["export_button"])
} }
Button(onClick = { Button(onClick = {
runCatching { runCatching {
@ -212,10 +215,10 @@ class HomeSettings : Routes.Route() {
context.log.error("Failed to clear messages", it) context.log.error("Failed to clear messages", it)
context.longToast("Failed to clear messages! ${it.localizedMessage}") context.longToast("Failed to clear messages! ${it.localizedMessage}")
}.onSuccess { }.onSuccess {
context.shortToast("Done!") context.shortToast(translation["success_toast"])
} }
}) { }) {
Text(text = "Clear") Text(text = translation["clear_button"])
} }
} }
OutlinedButton( OutlinedButton(
@ -226,12 +229,12 @@ class HomeSettings : Routes.Route() {
routes.loggerHistory.navigate() routes.loggerHistory.navigate()
} }
) { ) {
Text(text = "View Message History") Text(translation["view_logger_history_button"])
} }
} }
} }
RowTitle(title = "Debug") RowTitle(title = translation["debug_title"])
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -275,10 +278,10 @@ class HomeSettings : Routes.Route() {
context.log.error("Failed to clear file", it) context.log.error("Failed to clear file", it)
context.longToast("Failed to clear file! ${it.localizedMessage}") context.longToast("Failed to clear file! ${it.localizedMessage}")
}.onSuccess { }.onSuccess {
context.shortToast("Done!") context.shortToast(translation["success_toast"])
} }
}) { }) {
Text(text = "Clear File") Text(translation["clear_button"])
} }
} }
ShiftedRow { ShiftedRow {

View File

@ -148,7 +148,7 @@ class LoggedStories : Routes.Route() {
} }
} }
}) { }) {
Text(text = "Open") Text(text = context.translation["button.open"])
} }
Button(onClick = { Button(onClick = {
@ -160,7 +160,7 @@ class LoggedStories : Routes.Route() {
) )
) )
}) { }) {
Text(text = "Download") Text(text = context.translation["button.download"])
} }
if (remember { if (remember {
@ -180,7 +180,7 @@ class LoggedStories : Routes.Route() {
) )
) )
}) { }) {
Text(text = "Save from cache") Text(text = translation["save_from_cache_button"])
} }
} }
} }
@ -190,7 +190,7 @@ class LoggedStories : Routes.Route() {
} }
if (stories.isEmpty()) { if (stories.isEmpty()) {
Text(text = "No stories found", Modifier.fillMaxWidth(), textAlign = TextAlign.Center) Text(text = translation["no_stories"], Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
} }
LazyVerticalGrid( LazyVerticalGrid(
@ -212,7 +212,7 @@ class LoggedStories : Routes.Route() {
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
if (hasFailed) { if (hasFailed) {
Text(text = "Failed to load", Modifier.padding(8.dp), fontSize = 10.sp) Text(text = translation["story_failed_to_load"], Modifier.padding(8.dp), fontSize = 10.sp)
} else { } else {
Image( Image(
painter = rememberAsyncImagePainter( painter = rememberAsyncImagePainter(

View File

@ -29,7 +29,6 @@ import kotlin.io.encoding.ExperimentalEncodingApi
class ManageScope: Routes.Route() { class ManageScope: Routes.Route() {
private val dialogs by lazy { AlertDialogs(context.translation) } private val dialogs by lazy { AlertDialogs(context.translation) }
private val translation by lazy { context.translation.getCategory("manager.sections.social") }
private fun deleteScope(scope: SocialScope, id: String, coroutineScope: CoroutineScope) { private fun deleteScope(scope: SocialScope, id: String, coroutineScope: CoroutineScope) {
when (scope) { when (scope) {
@ -56,7 +55,7 @@ class ManageScope: Routes.Route() {
deleteConfirmDialog = false deleteConfirmDialog = false
}) { }) {
remember { AlertDialogs(context.translation) }.ConfirmDialog( remember { AlertDialogs(context.translation) }.ConfirmDialog(
title = "Are you sure you want to delete this ${scope.key.lowercase()}?", title = translation.format("delete_scope_confirm_dialog_title", "scope" to context.translation["scopes.${scope.key}"]),
onDismiss = { deleteConfirmDialog = false }, onDismiss = { deleteConfirmDialog = false },
onConfirm = { onConfirm = {
deleteScope(scope, id, coroutineScope); deleteConfirmDialog = false deleteScope(scope, id, coroutineScope); deleteConfirmDialog = false
@ -94,7 +93,6 @@ class ManageScope: Routes.Route() {
SectionTitle(translation["rules_title"]) SectionTitle(translation["rules_title"])
ContentCard { ContentCard {
//manager anti features etc
MessagingRuleType.entries.forEach { ruleType -> MessagingRuleType.entries.forEach { ruleType ->
var ruleEnabled by remember { var ruleEnabled by remember {
mutableStateOf(rules.any { it.key == ruleType.key }) mutableStateOf(rules.any { it.key == ruleType.key })
@ -110,14 +108,17 @@ class ManageScope: Routes.Route() {
text = if (ruleType.listMode && ruleState != null) { text = if (ruleType.listMode && ruleState != null) {
context.translation["rules.properties.${ruleType.key}.options.${ruleState.key}"] context.translation["rules.properties.${ruleType.key}.options.${ruleState.key}"]
} else context.translation["rules.properties.${ruleType.key}.name"], } else context.translation["rules.properties.${ruleType.key}.name"],
modifier = Modifier.weight(1f).padding(start = 5.dp, end = 5.dp) modifier = Modifier
.weight(1f)
.padding(start = 5.dp, end = 5.dp)
) )
Switch(checked = ruleEnabled, Switch(checked = ruleEnabled,
enabled = if (ruleType.listMode) ruleState != null else true, enabled = if (ruleType.listMode) ruleState != null else true,
onCheckedChange = { onCheckedChange = {
context.modDatabase.setRule(id, ruleType.key, it) context.modDatabase.setRule(id, ruleType.key, it)
ruleEnabled = it ruleEnabled = it
}) }
)
} }
} }
} }
@ -155,8 +156,7 @@ class ManageScope: Routes.Route() {
) )
} }
//need to display all units? private fun computeStreakETA(timestamp: Long): String? {
private fun computeStreakETA(timestamp: Long): String {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
val diff = timestamp - now val diff = timestamp - now
@ -180,7 +180,7 @@ class ManageScope: Routes.Route() {
stringBuilder.append("$seconds seconds ") stringBuilder.append("$seconds seconds ")
return stringBuilder.toString() return stringBuilder.toString()
} }
return "Expired" return null
} }
@OptIn(ExperimentalEncodingApi::class) @OptIn(ExperimentalEncodingApi::class)
@ -234,7 +234,7 @@ class ManageScope: Routes.Route() {
put("id", id) put("id", id)
} }
}) { }) {
Text("Show Logged Stories") Text(translation["logged_stories_button"])
} }
} }
@ -259,10 +259,11 @@ class ManageScope: Routes.Route() {
), maxLines = 1 ), maxLines = 1
) )
Text( Text(
text = translation.format( text = computeStreakETA(streaks.expirationTimestamp)?.let { translation.format(
"streaks_expiration_text", "streaks_expiration_text",
"eta" to computeStreakETA(streaks.expirationTimestamp) "eta" to it
), maxLines = 1 ) } ?: translation["streaks_expiration_text_expired"],
maxLines = 1
) )
} }
Row( Row(
@ -282,7 +283,6 @@ class ManageScope: Routes.Route() {
} }
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// e2ee section
if (context.config.root.experimental.e2eEncryption.globalState == true) { if (context.config.root.experimental.e2eEncryption.globalState == true) {
SectionTitle(translation["e2ee_title"]) SectionTitle(translation["e2ee_title"])
@ -362,7 +362,6 @@ class ManageScope: Routes.Route() {
return return
} }
Column( Column(
modifier = Modifier modifier = Modifier
.padding(10.dp) .padding(10.dp)

View File

@ -47,7 +47,6 @@ class MessagingPreview: Routes.Route() {
private lateinit var messagingBridge: MessagingBridge private lateinit var messagingBridge: MessagingBridge
private lateinit var previewScrollState: LazyListState private lateinit var previewScrollState: LazyListState
private val myUserId by lazy { messagingBridge.myUserId }
private val contentTypeTranslation by lazy { context.translation.getCategory("content_type") } private val contentTypeTranslation by lazy { context.translation.getCategory("content_type") }
private var messages = mutableStateListOf<Message>() private var messages = mutableStateListOf<Message>()
@ -117,7 +116,7 @@ class MessagingPreview: Routes.Route() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(5.dp) verticalArrangement = Arrangement.spacedBy(5.dp)
) { ) {
Text("Choose content types to process") Text(context.translation["manager.dialogs.messaging_action.title"])
Spacer(modifier = Modifier.height(5.dp)) Spacer(modifier = Modifier.height(5.dp))
availableTypes.forEach { contentType -> availableTypes.forEach { contentType ->
Row( Row(
@ -135,7 +134,7 @@ class MessagingPreview: Routes.Route() {
enabled = !selectAllState, enabled = !selectAllState,
onCheckedChange = { toggleContentType(contentType) } onCheckedChange = { toggleContentType(contentType) }
) )
Text(text = contentType.toString()) Text(text = contentTypeTranslation[contentType.name])
} }
} }
Row( Row(
@ -148,7 +147,7 @@ class MessagingPreview: Routes.Route() {
Switch(checked = selectAllState, onCheckedChange = { Switch(checked = selectAllState, onCheckedChange = {
selectAllState = it selectAllState = it
}) })
Text(text = "Select all") Text(text = context.translation["manager.dialogs.messaging_action.select_all_button"])
} }
Row( Row(
modifier = Modifier modifier = Modifier
@ -156,13 +155,13 @@ class MessagingPreview: Routes.Route() {
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
Button(onClick = { onDismiss() }) { Button(onClick = { onDismiss() }) {
Text("Cancel") Text(context.translation["button.cancel"])
} }
Button(onClick = { Button(onClick = {
onChoose(if (selectAllState) ContentType.entries.toTypedArray() onChoose(if (selectAllState) ContentType.entries.toTypedArray()
else selectedTypes.toTypedArray()) else selectedTypes.toTypedArray())
}) { }) {
Text("Continue") Text(context.translation["button.ok"])
} }
} }
} }
@ -286,28 +285,28 @@ class MessagingPreview: Routes.Route() {
shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(50.dp)) shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(50.dp))
) { ) {
DropdownMenu( DropdownMenu(
expanded = taskSelectionDropdown, onDismissRequest = { taskSelectionDropdown = false } expanded = taskSelectionDropdown && messages.isNotEmpty(), onDismissRequest = { taskSelectionDropdown = false }
) { ) {
val hasSelection = selectedMessages.isNotEmpty() val hasSelection = selectedMessages.isNotEmpty()
ActionButton(text = if (hasSelection) "Save selection" else "Save all", icon = Icons.Rounded.BookmarkAdded) { ActionButton(text = translation[if (hasSelection) "save_selection_option" else "save_all_option"], icon = Icons.Rounded.BookmarkAdded) {
launchMessagingTask(MessagingTaskType.SAVE) launchMessagingTask(MessagingTaskType.SAVE)
if (hasSelection) runCurrentTask() if (hasSelection) runCurrentTask()
else selectConstraintsDialog = true else selectConstraintsDialog = true
} }
ActionButton(text = if (hasSelection) "Unsave selection" else "Unsave all", icon = Icons.Rounded.BookmarkBorder) { ActionButton(text = translation[if (hasSelection) "unsave_selection_option" else "unsave_all_option"], icon = Icons.Rounded.BookmarkBorder) {
launchMessagingTask(MessagingTaskType.UNSAVE) launchMessagingTask(MessagingTaskType.UNSAVE)
if (hasSelection) runCurrentTask() if (hasSelection) runCurrentTask()
else selectConstraintsDialog = true else selectConstraintsDialog = true
} }
ActionButton(text = if (hasSelection) "Mark selected Snap as seen" else "Mark all Snaps as seen", icon = Icons.Rounded.RemoveRedEye) { ActionButton(text = translation[if (hasSelection) "mark_selection_as_seen_option" else "mark_all_as_seen_option"], icon = Icons.Rounded.RemoveRedEye) {
launchMessagingTask(MessagingTaskType.READ, listOf( launchMessagingTask(MessagingTaskType.READ, listOf(
MessagingConstraints.NO_USER_ID(myUserId), MessagingConstraints.NO_USER_ID(messagingBridge.myUserId),
MessagingConstraints.CONTENT_TYPE(arrayOf(ContentType.SNAP)) MessagingConstraints.CONTENT_TYPE(arrayOf(ContentType.SNAP))
)) ))
runCurrentTask() runCurrentTask()
} }
ActionButton(text = if (hasSelection) "Delete selected" else "Delete all", icon = Icons.Rounded.DeleteForever) { ActionButton(text = translation[if (hasSelection) "delete_selection_option" else "delete_all_option"], icon = Icons.Rounded.DeleteForever) {
launchMessagingTask(MessagingTaskType.DELETE, listOf(MessagingConstraints.USER_ID(myUserId))) { message -> launchMessagingTask(MessagingTaskType.DELETE, listOf(MessagingConstraints.USER_ID(messagingBridge.myUserId))) { message ->
coroutineScope.launch { coroutineScope.launch {
message.contentType = ContentType.STATUS.id message.contentType = ContentType.STATUS.id
} }
@ -377,7 +376,7 @@ class MessagingPreview: Routes.Route() {
.padding(40.dp), .padding(40.dp),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
Text("No messages") Text(translation["no_message_hint"])
} }
} }
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
@ -428,12 +427,7 @@ class MessagingPreview: Routes.Route() {
conversationId!!, conversationId!!,
20, 20,
lastMessageId lastMessageId
)?.reversed() )?.reversed() ?: throw IllegalStateException("Failed to fetch messages. Bridge returned null")
if (queriedMessages == null) {
context.shortToast("Failed to fetch messages")
return@cs
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
messages.addAll(queriedMessages) messages.addAll(queriedMessages)
@ -441,7 +435,7 @@ class MessagingPreview: Routes.Route() {
} }
}.onFailure { }.onFailure {
context.log.error("Failed to fetch messages", it) context.log.error("Failed to fetch messages", it)
context.shortToast("Failed to fetch messages: ${it.message}") context.shortToast(translation["message_fetch_failed"])
} }
} }
} }
@ -451,11 +445,7 @@ class MessagingPreview: Routes.Route() {
runCatching { runCatching {
messagingBridge = context.bridgeService!!.messagingBridge!! messagingBridge = context.bridgeService!!.messagingBridge!!
conversationId = if (scope == SocialScope.FRIEND) messagingBridge.getOneToOneConversationId(scopeId) else scopeId conversationId = (if (scope == SocialScope.FRIEND) messagingBridge.getOneToOneConversationId(scopeId) else scopeId) ?: throw IllegalStateException("Failed to get conversation id")
if (conversationId == null) {
context.longToast("Failed to fetch conversation id")
return
}
if (runCatching { !messagingBridge.isSessionStarted }.getOrDefault(true)) { if (runCatching { !messagingBridge.isSessionStarted }.getOrDefault(true)) {
context.androidContext.packageManager.getLaunchIntentForPackage( context.androidContext.packageManager.getLaunchIntentForPackage(
Constants.SNAPCHAT_PACKAGE_NAME Constants.SNAPCHAT_PACKAGE_NAME
@ -474,7 +464,7 @@ class MessagingPreview: Routes.Route() {
} }
fetchNewMessages() fetchNewMessages()
}.onFailure { }.onFailure {
context.longToast("Failed to initialize messaging bridge") context.longToast(translation["bridge_init_failed"])
context.log.error("Failed to initialize messaging bridge", it) context.log.error("Failed to initialize messaging bridge", it)
} }
} }
@ -511,7 +501,7 @@ class MessagingPreview: Routes.Route() {
.fillMaxSize() .fillMaxSize()
) { ) {
if (hasBridgeError) { if (hasBridgeError) {
Text("Failed to connect to Snapchat through bridge service") Text(translation["bridge_connection_failed"])
} }
if (!isBridgeConnected && !hasBridgeError) { if (!isBridgeConnected && !hasBridgeError) {

View File

@ -164,8 +164,8 @@ class SocialRoot : Routes.Route() {
else MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary
) )
Text( Text(
text = context.translation.format( text = translation.format(
"manager.sections.social.streaks_expiration_short", "streaks_expiration_short",
"hours" to (((streaks.expirationTimestamp - System.currentTimeMillis()) / 3600000).toInt().takeIf { it > 0 } ?: 0) "hours" to (((streaks.expirationTimestamp - System.currentTimeMillis()) / 3600000).toInt().takeIf { it > 0 } ?: 0)
.toString() .toString()
), ),

View File

@ -44,9 +44,9 @@ class AlertDialogs(
@Composable @Composable
fun DefaultDialogCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { fun DefaultDialogCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
Card( Card(
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.large,
modifier = Modifier modifier = Modifier
.padding(10.dp, 5.dp, 10.dp, 10.dp) .padding(16.dp)
.then(modifier), .then(modifier),
) { ) {
Column( Column(

View File

@ -21,6 +21,11 @@
} }
}, },
"scopes": {
"friend": "Friend",
"group": "Group"
},
"manager": { "manager": {
"routes": { "routes": {
"tasks": "Tasks", "tasks": "Tasks",
@ -37,27 +42,92 @@
}, },
"sections": { "sections": {
"home": { "home": {
"logs": { "update_title": "SnapEnhance Update",
"update_content": "Version {version} is available!",
"update_button": "Download"
},
"home_logs": {
"no_logs_hint": "No logs available",
"clear_logs_button": "Clear Logs", "clear_logs_button": "Clear Logs",
"export_logs_button": "Export Logs" "export_logs_button": "Export Logs",
} "saving_logs_toast": "Saving logs, this may take a while ...",
"saved_logs_success_toast": "Logs saved successfully",
"saved_logs_failure_toast": "Failed to save logs"
},
"home_settings": {
"actions_title": "Actions",
"message_logger_title": "Message Logger",
"debug_title": "Debug",
"success_toast": "Done!",
"message_logger_summary": "{messageCount} messages\n{storyCount} stories",
"export_button": "Export",
"clear_button": "Clear",
"view_logger_history_button": "View Logger History"
}, },
"tasks": { "tasks": {
"no_tasks": "No tasks" "no_tasks": "No tasks",
"merge_files_toast": "Merging {count} files",
"remove_selected_tasks_title": "Are you sure you want to remove selected tasks?",
"remove_all_tasks_title": "Are you sure you want to remove all tasks?",
"delete_files_option": "Also delete files",
"remove_selected_tasks_confirm": "Remove {count} tasks?",
"remove_all_tasks_confirm": "Remove all tasks?"
}, },
"features": { "features": {
"disabled": "Disabled" "disabled": "Disabled",
"export_option": "Export",
"import_option": "Import",
"reset_option": "Reset",
"config_export_success_toast": "Config exported successfully",
"config_import_success_toast": "Config imported successfully",
"config_import_failure_toast": "Failed to import config {error}",
"saved_config_snackbar": "Config saved"
}, },
"social": { "social": {
"streaks_expiration_short": "{hours}h"
},
"manage_scope": {
"logged_stories_button": "Show Logged Stories",
"e2ee_title": "End-to-End Encryption", "e2ee_title": "End-to-End Encryption",
"rules_title": "Rules", "rules_title": "Rules",
"participants_text": "{count} participants", "participants_text": "{count} participants",
"not_found": "Not found", "not_found": "Not found",
"streaks_title": "Streaks", "streaks_title": "Streaks",
"streaks_length_text": "Length: {length}", "streaks_length_text": "Length: {length}",
"streaks_expiration_short": "{hours}h",
"streaks_expiration_text": "Expires in {eta}", "streaks_expiration_text": "Expires in {eta}",
"reminder_button": "Set Reminder" "streaks_expiration_text_expired": "Expired",
"reminder_button": "Set Reminder",
"delete_scope_confirm_dialog_title": "Are you sure you want to delete a {scope}?"
},
"logged_stories": {
"story_failed_to_load": "Failed to load",
"no_stories": "No stories found",
"save_from_cache_button": "Save from Cache"
},
"messaging_preview": {
"bridge_connection_failed": "Failed to connect to Snapchat through bridge service",
"bridge_init_failed": "Failed to initialize messaging bridge",
"message_fetch_failed": "Failed to fetch messages",
"no_message_hint": "No message",
"save_selection_option": "Save Selection",
"save_all_option": "Save All",
"unsave_selection_option": "Unsave Selection",
"unsave_all_option": "Unsave All",
"mark_selection_as_seen_option": "Mark selected Snap as seen",
"mark_all_as_seen_option": "Mark all Snaps as seen",
"delete_selection_option": "Delete Selection",
"delete_all_option": "Delete All"
},
"logger_history": {
"list_friend_format": "Friend {name}",
"list_group_format": "Group {name}",
"no_more_messages": "No more messages",
"reverse_order_checkbox": "Reverse Order",
"chat_attachment": "Attachment {index}",
"empty_message": "Empty Chat Message",
"message_parse_failed": "Failed to parse message",
"unknown_sender": "Unknown Sender",
"download_attachment_failed_toast": "Failed to download attachment"
} }
}, },
"dialogs": { "dialogs": {
@ -71,6 +141,15 @@
"scripting_warning": { "scripting_warning": {
"title": "Warning", "title": "Warning",
"content": "SnapEnhance includes a scripting tool, allowing the execution of user-defined code on your device. Use extreme caution and only install modules from known, reliable sources. Unauthorized or unverified modules may pose security risks to your system." "content": "SnapEnhance includes a scripting tool, allowing the execution of user-defined code on your device. Use extreme caution and only install modules from known, reliable sources. Unauthorized or unverified modules may pose security risks to your system."
},
"reset_config": {
"title": "Reset config",
"content": "Are you sure you want to reset the config?",
"success_toast": "Config reset successfully"
},
"messaging_action": {
"title": "Choose content types to process",
"select_all_button": "Select All"
} }
} }
}, },