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

@ -8,8 +8,8 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
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
@ -20,8 +20,8 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.rhunk.snapenhance.bridge.DownloadCallback import me.rhunk.snapenhance.bridge.DownloadCallback
@ -36,8 +36,8 @@ import me.rhunk.snapenhance.task.PendingTaskListener
import me.rhunk.snapenhance.task.Task import me.rhunk.snapenhance.task.Task
import me.rhunk.snapenhance.task.TaskStatus import me.rhunk.snapenhance.task.TaskStatus
import me.rhunk.snapenhance.task.TaskType import me.rhunk.snapenhance.task.TaskType
import me.rhunk.snapenhance.ui.manager.Routes import me.rhunk.snapenhance.ui.manager.Routes
import me.rhunk.snapenhance.ui.util.OnLifecycleEvent import me.rhunk.snapenhance.ui.util.OnLifecycleEvent
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -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 = {
context.config.reset() Button(
context.shortToast("Config successfully reset!") onClick = {
}, context.config.reset()
onDismiss = { showResetConfirmationDialog = false } context.shortToast(context.translation["manager.dialogs.reset_config.success_toast"])
) 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") }
} }
},
modifier = Modifier.padding(10.dp), OnLifecycleEvent { _, event ->
containerColor = MaterialTheme.colorScheme.primary, if (event == Lifecycle.Event.ON_PAUSE || event == Lifecycle.Event.ON_STOP) {
contentColor = MaterialTheme.colorScheme.onPrimary, saveConfig()
shape = RoundedCornerShape(16.dp), }
) { }
Icon(
imageVector = Icons.Rounded.Save, DisposableEffect(Unit) {
contentDescription = null onDispose {
) saveConfig()
}
} }
} }
@ -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.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use { context.coroutineScope.launch {
runCatching { context.shortToast(translation["saving_logs_toast"])
context.log.exportLogsToZip(it) context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use {
context.longToast("Saved logs to $uri") runCatching {
}.onFailure { context.log.exportLogsToZip(it)
context.longToast("Failed to save logs to $uri!") context.longToast(translation["saved_logs_success_toast"])
context.log.error("Failed to save logs to $uri!", it) }.onFailure {
context.longToast(translation["saved_logs_failure_toast"])
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",
"clear_logs_button": "Clear Logs", "update_content": "Version {version} is available!",
"export_logs_button": "Export Logs" "update_button": "Download"
} },
"home_logs": {
"no_logs_hint": "No logs available",
"clear_logs_button": "Clear 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"
} }
} }
}, },