last commit before migration from GitHub

This commit is contained in:
particle-box
2026-01-31 07:55:39 +05:30
parent c9052cba26
commit c5f164ccc8
38 changed files with 720 additions and 261 deletions

View File

@@ -268,7 +268,7 @@ class RemoteSideContext(
Constants.SNAPCHAT_PACKAGE_NAME Constants.SNAPCHAT_PACKAGE_NAME
) )
if (intent == null) { if (intent == null) {
shortToast("Can't execute action: Snapchat is not installed") shortToast(translation["toast_snapchat_not_installed"])
return return
} }
intent.putExtra(EnumAction.ACTION_PARAMETER, action.key) intent.putExtra(EnumAction.ACTION_PARAMETER, action.key)

View File

@@ -91,6 +91,7 @@ object UpdateDownloader {
scope: CoroutineScope scope: CoroutineScope
) { ) {
val context = remoteContext.androidContext val context = remoteContext.androidContext
val translation = remoteContext.translation.getCategory("manager.sections.home")
val fetch = getInstance(remoteContext) val fetch = getInstance(remoteContext)
val filePath = File(context.externalCacheDir, fileName).path val filePath = File(context.externalCacheDir, fileName).path
remoteContext.log.info("Starting update download from $downloadUrl -> $filePath", TAG) remoteContext.log.info("Starting update download from $downloadUrl -> $filePath", TAG)
@@ -107,7 +108,7 @@ object UpdateDownloader {
override fun onQueued(download: Download, waitingOnNetwork: Boolean) { override fun onQueued(download: Download, waitingOnNetwork: Boolean) {
downloadState.value = DownloadState.DOWNLOADING downloadState.value = DownloadState.DOWNLOADING
Toast.makeText(context, "Download started", Toast.LENGTH_SHORT).show() Toast.makeText(context, translation["update_download_started_toast"], Toast.LENGTH_SHORT).show()
} }
override fun onProgress(download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long) { override fun onProgress(download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long) {
@@ -122,7 +123,7 @@ object UpdateDownloader {
"Download completed -> ${downloadedFile.absolutePath} (${downloadedFile.length()} bytes)", "Download completed -> ${downloadedFile.absolutePath} (${downloadedFile.length()} bytes)",
TAG TAG
) )
Toast.makeText(context, "Download completed", Toast.LENGTH_SHORT).show() Toast.makeText(context, translation["update_download_completed_toast"], Toast.LENGTH_SHORT).show()
val apkFile = resolveDownloadedApk(remoteContext, downloadedFile) val apkFile = resolveDownloadedApk(remoteContext, downloadedFile)
val uri = FileProvider.getUriForFile( val uri = FileProvider.getUriForFile(
context, context,
@@ -144,7 +145,7 @@ object UpdateDownloader {
remoteContext.log.info("Cleaned downloaded update files", TAG) remoteContext.log.info("Cleaned downloaded update files", TAG)
} }
}.onFailure { }.onFailure {
Toast.makeText(context, "Failed to install update. Check logs for more details.", Toast.LENGTH_SHORT).show() Toast.makeText(context, translation["update_install_failed_toast"], Toast.LENGTH_SHORT).show()
remoteContext.log.error("Failed to install downloaded update", it, TAG) remoteContext.log.error("Failed to install downloaded update", it, TAG)
downloadState.value = DownloadState.FAILED downloadState.value = DownloadState.FAILED
} }
@@ -157,7 +158,11 @@ object UpdateDownloader {
override fun onError(download: Download, error: Error, throwable: Throwable?) { override fun onError(download: Download, error: Error, throwable: Throwable?) {
downloadState.value = DownloadState.FAILED downloadState.value = DownloadState.FAILED
Toast.makeText(context, "Download failed: $error", Toast.LENGTH_SHORT).show() Toast.makeText(
context,
translation.format("update_download_failed_toast", "error" to error.toString()),
Toast.LENGTH_SHORT
).show()
throwable?.let { remoteContext.log.error("Update download failed: $error", it, TAG) } throwable?.let { remoteContext.log.error("Update download failed: $error", it, TAG) }
?: remoteContext.log.error("Update download failed: $error", TAG) ?: remoteContext.log.error("Update download failed: $error", TAG)
fetch.removeListener(this) fetch.removeListener(this)

View File

@@ -414,7 +414,7 @@ class TasksRootSection : Routes.Route() {
IconButton(onClick = { IconButton(onClick = {
showConfirmDialog = true showConfirmDialog = true
}) { }) {
Icon(Icons.Filled.Delete, contentDescription = "Clear tasks") Icon(Icons.Filled.Delete, contentDescription = translation["clear_button_description"])
} }
if (showConfirmDialog) { if (showConfirmDialog) {
@@ -917,7 +917,18 @@ class TasksRootSection : Routes.Route() {
fontSize = 18.sp fontSize = 18.sp
) )
Text( Text(
text = if (activeTasks.isNotEmpty()) "${activeTasks.size} active · ${recentTasks.size} recent" else "Idle · ${recentTasks.size} recent", text = if (activeTasks.isNotEmpty()) {
translation.format(
"summary_active",
"active" to activeTasks.size.toString(),
"recent" to recentTasks.size.toString()
)
} else {
translation.format(
"summary_idle",
"recent" to recentTasks.size.toString()
)
},
color = PurrfectPalette.textSecondary, color = PurrfectPalette.textSecondary,
fontSize = 12.sp fontSize = 12.sp
) )
@@ -964,11 +975,16 @@ class TasksRootSection : Routes.Route() {
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Icon(Icons.Filled.PlaylistAddCheckCircle, contentDescription = null, tint = Color.White) Icon(Icons.Filled.PlaylistAddCheckCircle, contentDescription = null, tint = Color.White)
Text(text = "${activeTasks.size} running", color = Color.White, fontWeight = FontWeight.SemiBold, fontSize = 12.sp) Text(
text = translation.format("running_count", "count" to activeTasks.size.toString()),
color = Color.White,
fontWeight = FontWeight.SemiBold,
fontSize = 12.sp
)
} }
} }
IconButton(onClick = { showConfirmDialog = true }) { IconButton(onClick = { showConfirmDialog = true }) {
Icon(Icons.Filled.Delete, contentDescription = "Clear tasks", tint = Color.White) Icon(Icons.Filled.Delete, contentDescription = translation["clear_button_description"], tint = Color.White)
} }
} }
} }

View File

@@ -203,7 +203,7 @@ class ManageRuleFeature : Routes.Route() {
title = translation["clear_list_button"], title = translation["clear_list_button"],
text = translation["dialog_clear_confirmation_text"], text = translation["dialog_clear_confirmation_text"],
icon = Icons.Default.DeleteSweep, icon = Icons.Default.DeleteSweep,
confirmButtonText = "Clear", confirmButtonText = context.translation["clear"],
dismissButtonText = context.translation["button.cancel"], dismissButtonText = context.translation["button.cancel"],
onDismiss = { confirmationDialog = false }, onDismiss = { confirmationDialog = false },
onConfirm = { onConfirm = {
@@ -390,7 +390,7 @@ class ManageRuleFeature : Routes.Route() {
contentColor = Color.White contentColor = Color.White
) )
) { ) {
Text(text = "Clear") Text(text = context.translation["clear"])
} }
} }
} }

View File

@@ -65,13 +65,7 @@ class HomeAbout : Routes.Route() {
FontFamily(Font(R.font.avenir_next_medium, FontWeight.Medium)) FontFamily(Font(R.font.avenir_next_medium, FontWeight.Medium))
} }
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val aboutStory = remember { val aboutStory = remember { translation["about_story"] }
"""
PurrfectSnap was founded on 2nd of October, 2025, as a fork of SnapEnhance by ΞTΞRNAL with a vision to provide users the quality Snapchat experience they deserve. This app was just meant to be a minor update in the SnapEnhance repository, but it soon became a separate app wherein the contributors kept adding features. Then the developer <RSR/> joined the team, and this app soon became a huge success. We received much love and support and gained 1K+ downloads in just two days! We thank all users and contributors; without your support, we wouldn't have reached this place. We would also like to convey our huge thanks to rhunk, the lead developer of SnapEnhance, as without him, this app wouldn't even exist. We are immensely grateful to him. Lastly, we would like to thank all our admins, notably: CLASSIC GENIUS, Harry, SUJΛL, Zain & scrodingerspet, who were right there with us from the very beginning. We would also like to thank all testers, notably Leo & Toxic, who tested and reported bugs continuously. We are immensely grateful for your contribution.
""".trimIndent()
}
val pagePadding = 16.dp val pagePadding = 16.dp
val bottomPadding = routes.bottomPadding + val bottomPadding = routes.bottomPadding +
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +
@@ -82,7 +76,7 @@ class HomeAbout : Routes.Route() {
val lastTapTime = remember { mutableLongStateOf(0L) } val lastTapTime = remember { mutableLongStateOf(0L) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
context.shortToast("Tap 5 times in this screen to see some magic 😉!") context.shortToast(translation["about_magic_toast"])
} }
Box( Box(
@@ -97,7 +91,7 @@ class HomeAbout : Routes.Route() {
.padding(bottom = bottomPadding) .padding(bottom = bottomPadding)
) { ) {
FloatingTopBar( FloatingTopBar(
title = routeInfo.translatedKey?.value ?: "About", title = routeInfo.translatedKey?.value ?: translation["manager.routes.home_about"],
onBack = { routes.navController.popBackStack() } onBack = { routes.navController.popBackStack() }
) )
@@ -121,7 +115,7 @@ class HomeAbout : Routes.Route() {
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Text( Text(
text = "PurrfectSnap", text = translation["about_title"],
fontSize = 28.sp, fontSize = 28.sp,
fontWeight = FontWeight.ExtraBold, fontWeight = FontWeight.ExtraBold,
color = PurrfectPalette.textPrimary, color = PurrfectPalette.textPrimary,
@@ -143,14 +137,14 @@ class HomeAbout : Routes.Route() {
} }
) )
Text( Text(
text = "An Xposed Module meant to enhance your Snapchat experience!", text = translation["about_tagline"],
fontSize = 13.sp, fontSize = 13.sp,
color = PurrfectPalette.textSecondary, color = PurrfectPalette.textSecondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Text( Text(
text = "Lead Developers", text = translation["about_lead_developers_title"],
fontSize = 15.sp, fontSize = 15.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White, color = Color.White,
@@ -196,7 +190,7 @@ class HomeAbout : Routes.Route() {
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
Text( Text(
text = "Our Story", text = translation["about_story_title"],
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = Color.White color = Color.White
@@ -228,7 +222,7 @@ class HomeAbout : Routes.Route() {
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(
text = "With love, PurrfectSnap Team", text = translation["about_thanks_title"],
fontSize = 15.sp, fontSize = 15.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White, color = Color.White,
@@ -242,7 +236,12 @@ class HomeAbout : Routes.Route() {
) { ) {
Button( Button(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = { context.androidContext.openLink("https://github.com/particle-box/PurrfectSnap") }, onClick = {
context.androidContext.openLink(
"https://github.com/particle-box/PurrfectSnap",
context.translation["toast_open_link_failed"]
)
},
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color.White, containerColor = Color.White,
contentColor = Color(0xFF1B152E) contentColor = Color(0xFF1B152E)
@@ -254,11 +253,16 @@ class HomeAbout : Routes.Route() {
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(text = "GitHub", maxLines = 1, overflow = TextOverflow.Ellipsis) Text(text = translation["github_button"], maxLines = 1, overflow = TextOverflow.Ellipsis)
} }
OutlinedButton( OutlinedButton(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = { context.androidContext.openLink("https://t.me/purrfectsnap_official") }, onClick = {
context.androidContext.openLink(
"https://t.me/purrfectsnap_official",
context.translation["toast_open_link_failed"]
)
},
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.35f)), border = BorderStroke(1.dp, Color.White.copy(alpha = 0.35f)),
colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White) colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White)
) { ) {
@@ -269,7 +273,7 @@ class HomeAbout : Routes.Route() {
tint = Color.White tint = Color.White
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(text = "Telegram", maxLines = 1, overflow = TextOverflow.Ellipsis) Text(text = translation["telegram_button"], maxLines = 1, overflow = TextOverflow.Ellipsis)
} }
} }
} }

View File

@@ -127,7 +127,7 @@ class HomeLogs : Routes.Route() {
} }
} }
readerResult.onFailure { readerResult.onFailure {
context.longToast("Failed to read logs!") context.longToast(translation["read_logs_failed_toast"])
} }
readerResult.getOrNull()?.let { reader -> readerResult.getOrNull()?.let { reader ->
logReader = reader logReader = reader

View File

@@ -319,7 +319,7 @@ class HomeRootSection : Routes.Route() {
) { routes.homeLogs.navigate() } ) { routes.homeLogs.navigate() }
TopBarActionChip( TopBarActionChip(
icon = Icons.Filled.Info, icon = Icons.Filled.Info,
label = "About" label = translation["manager.routes.home_about"]
) { routes.about.navigate() } ) { routes.about.navigate() }
} }
@@ -453,7 +453,7 @@ class HomeRootSection : Routes.Route() {
fontFamily = avenirNext fontFamily = avenirNext
) )
Text( Text(
text = "An Xposed Module meant to enhance your Snapchat experience", text = translation["hero_tagline"],
color = Color.White.copy(alpha = 0.9f), color = Color.White.copy(alpha = 0.9f),
fontSize = 15.sp, fontSize = 15.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
@@ -465,9 +465,9 @@ class HomeRootSection : Routes.Route() {
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
HeroBadge("Version: $versionName - $channelLabel") HeroBadge(translation.format("hero_version_label", "version" to versionName, "channel" to channelLabel))
gitHashShort.takeIf { it.isNotBlank() && it.lowercase() != "unknown" }?.let { gitHashShort.takeIf { it.isNotBlank() && it.lowercase() != "unknown" }?.let {
HeroBadge("Build: $it") HeroBadge(translation.format("hero_build_label", "build" to it))
} }
} }
@@ -566,7 +566,7 @@ class HomeRootSection : Routes.Route() {
tint = Color(0xFFA3F0C2) tint = Color(0xFFA3F0C2)
) )
Text( Text(
text = "Ready to install", text = translation["update_ready_label"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
@@ -611,7 +611,7 @@ class HomeRootSection : Routes.Route() {
.background(if (isPurrAuraActive) PurrfectPalette.glowPrimary else Color(0xFF8C8CA3)) .background(if (isPurrAuraActive) PurrfectPalette.glowPrimary else Color(0xFF8C8CA3))
) )
Text( Text(
text = if (isPurrAuraActive) "PurrAura Active!" else "PurrAura Inactive", text = if (isPurrAuraActive) translation["purr_aura_active_label"] else translation["purr_aura_inactive_label"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 14.sp fontSize = 14.sp
@@ -627,7 +627,7 @@ class HomeRootSection : Routes.Route() {
) { ) {
Icon(Icons.Filled.Settings, contentDescription = null, tint = Color.White) Icon(Icons.Filled.Settings, contentDescription = null, tint = Color.White)
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
Text("Open Settings") Text(translation["open_settings_button"])
} }
} }
} }
@@ -657,7 +657,7 @@ class HomeRootSection : Routes.Route() {
) { ) {
Icon(Icons.AutoMirrored.Filled.Help, contentDescription = null, modifier = Modifier.size(18.dp)) Icon(Icons.AutoMirrored.Filled.Help, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(text = "Wiki", maxLines = 1, overflow = TextOverflow.Ellipsis) Text(text = translation["wiki_button"], maxLines = 1, overflow = TextOverflow.Ellipsis)
} }
OutlinedButton( OutlinedButton(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@@ -672,7 +672,7 @@ class HomeRootSection : Routes.Route() {
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
Text(text = "GitHub", maxLines = 1, overflow = TextOverflow.Ellipsis) Text(text = translation["github_button"], maxLines = 1, overflow = TextOverflow.Ellipsis)
} }
ExternalLinkIcon( ExternalLinkIcon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_telegram), imageVector = ImageVector.vectorResource(id = R.drawable.ic_telegram),
@@ -740,7 +740,7 @@ class HomeRootSection : Routes.Route() {
context.database.getQuickTiles().filter { it.isNotBlank() } context.database.getQuickTiles().filter { it.isNotBlank() }
} }
val updateChannel = context.config.root.global.updateSettings.updateChannel.getNullable() ?: "stable" val updateChannel = context.config.root.global.updateSettings.updateChannel.getNullable() ?: "stable"
val channelLabel = if (updateChannel == "prerelease") "Pre-release" else "Stable" val channelLabel = if (updateChannel == "prerelease") translation["channel_label_prerelease"] else translation["channel_label_stable"]
val latestUpdate by rememberAsyncMutableState(defaultValue = null, keys = arrayOf(updateChannel)) { val latestUpdate by rememberAsyncMutableState(defaultValue = null, keys = arrayOf(updateChannel)) {
val channel = if (updateChannel == "prerelease") Channel.PRERELEASE else Channel.STABLE val channel = if (updateChannel == "prerelease") Channel.PRERELEASE else Channel.STABLE
Updater.getLatestRelease(channel) Updater.getLatestRelease(channel)
@@ -785,7 +785,7 @@ class HomeRootSection : Routes.Route() {
if (abiName == null) { if (abiName == null) {
android.widget.Toast.makeText( android.widget.Toast.makeText(
context.androidContext, context.androidContext,
"Your device architecture is not supported for automatic updates.", translation["update_arch_not_supported_toast"],
android.widget.Toast.LENGTH_LONG android.widget.Toast.LENGTH_LONG
).show() ).show()
} else { } else {
@@ -807,7 +807,10 @@ class HomeRootSection : Routes.Route() {
"No matching update asset for arch=$abiName (available: ${latest.assetDownloads.keys})", "No matching update asset for arch=$abiName (available: ${latest.assetDownloads.keys})",
"HomeRoot" "HomeRoot"
) )
context.androidContext.openLink(latest.releaseUrl) context.androidContext.openLink(
latest.releaseUrl,
context.translation["toast_open_link_failed"]
)
} }
} }
} }
@@ -926,7 +929,7 @@ class HomeRootSection : Routes.Route() {
TopBarActionChip( TopBarActionChip(
icon = Icons.Filled.Notifications, icon = Icons.Filled.Notifications,
label = null, label = null,
contentDescription = "Announcements" contentDescription = translation["announcements_button_description"]
) { ) {
showAnnouncementsDialog = true showAnnouncementsDialog = true
loadAnnouncements() loadAnnouncements()
@@ -949,9 +952,24 @@ class HomeRootSection : Routes.Route() {
onUpdateAction = onUpdateButtonClick, onUpdateAction = onUpdateButtonClick,
channelLabel = channelLabel, channelLabel = channelLabel,
isPurrAuraActive = isPurrAuraActive, isPurrAuraActive = isPurrAuraActive,
onWikiClick = { context.androidContext.openLink("https://github.com/particle-box/PurrfectSnap/wiki") }, onWikiClick = {
onTelegramClick = { context.androidContext.openLink("https://t.me/purrfectsnap_official") }, context.androidContext.openLink(
onGithubClick = { context.androidContext.openLink("https://github.com/particle-box/PurrfectSnap") }, "https://github.com/particle-box/PurrfectSnap/wiki",
context.translation["toast_open_link_failed"]
)
},
onTelegramClick = {
context.androidContext.openLink(
"https://t.me/purrfectsnap_official",
context.translation["toast_open_link_failed"]
)
},
onGithubClick = {
context.androidContext.openLink(
"https://github.com/particle-box/PurrfectSnap",
context.translation["toast_open_link_failed"]
)
},
authorName = "ETERNAL", authorName = "ETERNAL",
onManageClick = { routes.settings.navigate() }, onManageClick = { routes.settings.navigate() },
avenirNext = avenirNext, avenirNext = avenirNext,
@@ -1001,14 +1019,14 @@ class HomeRootSection : Routes.Route() {
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = "No quick tiles yet", text = translation["quick_actions_empty_title"],
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = Color.White color = Color.White
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = "Design your dream grid with the actions you use the most.", text = translation["quick_actions_empty_subtitle"],
fontSize = 14.sp, fontSize = 14.sp,
color = Color.White.copy(alpha = 0.75f), color = Color.White.copy(alpha = 0.75f),
textAlign = TextAlign.Center textAlign = TextAlign.Center
@@ -1027,7 +1045,7 @@ class HomeRootSection : Routes.Route() {
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
Text(text = "Add tile") Text(text = translation["quick_actions_add_tile_button"])
} }
} }
} else { } else {
@@ -1048,7 +1066,7 @@ class HomeRootSection : Routes.Route() {
overflow = TextOverflow.Clip overflow = TextOverflow.Clip
) )
Text( Text(
text = "${selectedTiles.size} curated shortcuts", text = translation.format("quick_actions_count_label", "count" to selectedTiles.size.toString()),
fontSize = 13.sp, fontSize = 13.sp,
color = Color.White.copy(alpha = 0.75f), color = Color.White.copy(alpha = 0.75f),
textAlign = TextAlign.Center textAlign = TextAlign.Center
@@ -1069,7 +1087,7 @@ class HomeRootSection : Routes.Route() {
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
Text(text = "Manage") Text(text = translation["quick_actions_manage_button"])
} }
} }
} }

View File

@@ -661,7 +661,12 @@ class HomeSettings : Routes.Route() {
} }
}.onFailure { }.onFailure {
context.log.error("Failed to open file", it) context.log.error("Failed to open file", it)
context.longToast("Failed to open file! ${it.localizedMessage}") context.longToast(
translation.format(
"open_file_failed_toast",
"message" to (it.localizedMessage ?: "")
)
)
} }
}, },
colors = sharedButtonColors, colors = sharedButtonColors,
@@ -728,12 +733,22 @@ class HomeSettings : Routes.Route() {
context.log.info("Imported message logger from $uri", "MessageLogger") context.log.info("Imported message logger from $uri", "MessageLogger")
}.onFailure { }.onFailure {
context.log.error("Failed to import message logger", it) context.log.error("Failed to import message logger", it)
context.longToast("Import failed: ${it.localizedMessage ?: it.message}") context.longToast(
translation.format(
"import_failed_toast",
"message" to (it.localizedMessage ?: it.message ?: "")
)
)
} }
} }
}.onFailure { }.onFailure {
context.log.error("Failed to launch import picker", it) context.log.error("Failed to launch import picker", it)
context.longToast("Import failed: ${it.localizedMessage ?: it.message}") context.longToast(
translation.format(
"import_failed_toast",
"message" to (it.localizedMessage ?: it.message ?: "")
)
)
} }
}) { }) {
Text(translation["button.import"] ?: "Import") Text(translation["button.import"] ?: "Import")

View File

@@ -183,7 +183,8 @@ fun ScriptCatalog(root: ScriptingRootSection) {
Button( Button(
onClick = { onClick = {
context.androidContext.openLink( context.androidContext.openLink(
"https://github.com/particle-box/PurrfectSnap/blob/dev/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ScriptRepos.md" "https://github.com/particle-box/PurrfectSnap/blob/dev/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ScriptRepos.md",
context.translation["toast_open_link_failed"]
) )
}, },
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(

View File

@@ -543,10 +543,24 @@ class ScriptingRootSection : Routes.Route() {
if (scriptingFolder == null) showToast = true else showImportDialog = true if (scriptingFolder == null) showToast = true else showImportDialog = true
}, },
onOpenFolder = { onOpenFolder = {
if (scriptingFolder == null) showToast = true else scriptingFolder?.let { context.androidContext.openLink(it.uri.toString()) } if (scriptingFolder == null) {
showToast = true
} else {
scriptingFolder?.let {
context.androidContext.openLink(
it.uri.toString(),
context.translation["toast_open_link_failed"]
)
}
}
}, },
onManageRepos = { routes.manageScriptRepos.navigate() }, onManageRepos = { routes.manageScriptRepos.navigate() },
onDocs = { context.androidContext.openLink("https://github.com/SnapEnhance/scripting-docs") }, onDocs = {
context.androidContext.openLink(
"https://github.com/SnapEnhance/scripting-docs",
context.translation["toast_open_link_failed"]
)
},
folderSelected = scriptingFolder != null folderSelected = scriptingFolder != null
) )
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))

View File

@@ -192,7 +192,7 @@ class LoggedStories : Routes.Route() {
content = context.imageLoader.diskCache?.openSnapshot(story.url)?.use { content = context.imageLoader.diskCache?.openSnapshot(story.url)?.use {
it.data.toFile().absolutePath it.data.toFile().absolutePath
} ?: run { } ?: run {
context.shortToast("Failed to get file") context.shortToast(translation["failed_to_get_file"])
return@Button return@Button
}, },
type = DownloadMediaType.LOCAL_MEDIA, type = DownloadMediaType.LOCAL_MEDIA,

View File

@@ -292,8 +292,7 @@ class SocialRootSection : Routes.Route() {
IconButton(onClick = { searchQuery = "" }) { IconButton(onClick = { searchQuery = "" }) {
Icon( Icon(
imageVector = Icons.Filled.Close, imageVector = Icons.Filled.Close,
contentDescription = context.translation["close_button_description"] contentDescription = translation["clear_search_button_description"],
?: "Clear search",
tint = Color.White tint = Color.White
) )
} }
@@ -532,12 +531,12 @@ class SocialRootSection : Routes.Route() {
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
StatPill(label = "Friends", value = friendCount) StatPill(label = translation["friends_tab"], value = friendCount)
StatPill(label = "Groups", value = groupCount) StatPill(label = translation["groups_tab"], value = groupCount)
IconButton(onClick = onSearchToggle) { IconButton(onClick = onSearchToggle) {
Icon( Icon(
imageVector = if (searchActive) Icons.Filled.Close else Icons.Filled.Search, imageVector = if (searchActive) Icons.Filled.Close else Icons.Filled.Search,
contentDescription = if (searchActive) "Close search" else "Search", contentDescription = if (searchActive) translation["close_search_button_description"] else translation["search_button_description"],
tint = Color.White tint = Color.White
) )
} }

View File

@@ -248,7 +248,9 @@ class FriendTrackerManagerRoot : Routes.Route() {
routes.friendTrackerConfigJsonForImport = content routes.friendTrackerConfigJsonForImport = content
routes.friendTrackerConfigImport.navigate() routes.friendTrackerConfigImport.navigate()
}.onFailure { }.onFailure {
context.longToast("Failed to read file: ${it.message}") context.longToast(
translation.format("read_file_failed_toast", "message" to (it.message ?: ""))
)
} }
} }
} }
@@ -591,7 +593,9 @@ class FriendTrackerManagerRoot : Routes.Route() {
routes.friendTrackerConfigJsonForImport = content routes.friendTrackerConfigJsonForImport = content
routes.friendTrackerConfigImport.navigate() routes.friendTrackerConfigImport.navigate()
}.onFailure { }.onFailure {
context.longToast("Failed to read file: ${it.message}") context.longToast(
translation.format("read_file_failed_toast", "message" to (it.message ?: ""))
)
} }
} }
} }

View File

@@ -206,6 +206,7 @@ class SetupActivity : ComponentActivity() {
setContent { setContent {
val context = LocalContext.current val context = LocalContext.current
val translation = setupContext.translation
val navController = rememberNavController() val navController = rememberNavController()
var canGoNext by remember { mutableStateOf(false) } var canGoNext by remember { mutableStateOf(false) }
var lastRoute by rememberSaveable { mutableStateOf("") } var lastRoute by rememberSaveable { mutableStateOf("") }
@@ -226,16 +227,16 @@ class SetupActivity : ComponentActivity() {
if (shouldShowAbiWarning) { if (shouldShowAbiWarning) {
AestheticDialog( AestheticDialog(
onDismissRequest = {}, onDismissRequest = {},
title = "Wrong APK installed", title = translation["setup.activity.wrong_apk_title"],
text = "", text = "",
icon = Icons.Filled.Warning, icon = Icons.Filled.Warning,
confirmButtonText = "Close", confirmButtonText = translation["setup.activity.close_button"],
onConfirm = { (context as? Activity)?.finishAffinity() }, onConfirm = { (context as? Activity)?.finishAffinity() },
showCloseButton = false, showCloseButton = false,
opaque = true, opaque = true,
customContent = { customContent = {
Text( Text(
text = "Your device is armv8, please download the armv8 apk, not armv7.", text = translation["setup.activity.wrong_apk_message"],
color = PurrfectPalette.textSecondary, color = PurrfectPalette.textSecondary,
lineHeight = 18.sp lineHeight = 18.sp
) )
@@ -344,7 +345,14 @@ class SetupActivity : ComponentActivity() {
.background(Color.Transparent) .background(Color.Transparent)
) { ) {
if (showImportantDialog) { if (showImportantDialog) {
val confirmLabel = if (importantTimeout > 0) "I understand (${importantTimeout}s)" else "I understand" val confirmLabel = if (importantTimeout > 0) {
translation.format(
"setup.activity.important_confirm_timeout",
"seconds" to importantTimeout.toString()
)
} else {
translation["setup.activity.important_confirm"]
}
AestheticDialog( AestheticDialog(
onDismissRequest = { onDismissRequest = {
if (importantTimeout == 0) { if (importantTimeout == 0) {
@@ -352,7 +360,7 @@ class SetupActivity : ComponentActivity() {
setupPrefs.edit().putBoolean("setup_important_notice_shown", true).apply() setupPrefs.edit().putBoolean("setup_important_notice_shown", true).apply()
} }
}, },
title = "Important!", title = translation["setup.activity.important_title"],
text = "", text = "",
icon = Icons.Filled.Warning, icon = Icons.Filled.Warning,
confirmButtonText = confirmLabel, confirmButtonText = confirmLabel,
@@ -366,7 +374,7 @@ class SetupActivity : ComponentActivity() {
showCloseButton = false, showCloseButton = false,
customContent = { customContent = {
Text( Text(
text = "If you have used SnapEnhance or any other mod besides PurrfectSnap, we recommend uninstalling everything and staying on stock Snapchat for one week. Then switch to PurrfectSnap after next Friday.", text = translation["setup.activity.important_message"],
color = PurrfectPalette.textSecondary, color = PurrfectPalette.textSecondary,
lineHeight = 18.sp lineHeight = 18.sp
) )
@@ -471,50 +479,50 @@ private fun SetupScreen.meta(context: RemoteSideContext): SetupStepMeta {
return when (this) { return when (this) {
is PickLanguageScreen -> SetupStepMeta( is PickLanguageScreen -> SetupStepMeta(
route = route, route = route,
title = translation["setup.dialogs.select_language"] ?: "Choose your language", title = translation["setup.dialogs.select_language"],
subtitle = "Tune PurrfectSnap to speak your voice before anything else.", subtitle = translation["setup.activity.language_subtitle"],
icon = Icons.Filled.Language icon = Icons.Filled.Language
) )
is InstallModeScreen -> SetupStepMeta( is InstallModeScreen -> SetupStepMeta(
route = route, route = route,
title = "Choose your device", title = translation["setup.activity.install_mode_title"],
subtitle = "Pick the path that matches how you'll install PurrfectSnap.", subtitle = translation["setup.activity.install_mode_subtitle"],
icon = Icons.Filled.VerifiedUser icon = Icons.Filled.VerifiedUser
) )
is PermissionsScreen -> SetupStepMeta( is PermissionsScreen -> SetupStepMeta(
route = route, route = route,
title = translation["setup.permissions.dialog"] ?: "Essential permissions", title = translation["setup.permissions.dialog"],
subtitle = "Grant the essentials so overlays, downloads, and alerts stay reliable.", subtitle = translation["setup.activity.permissions_subtitle"],
icon = Icons.Filled.VerifiedUser icon = Icons.Filled.VerifiedUser
) )
is PatchSnapchatScreen -> SetupStepMeta( is PatchSnapchatScreen -> SetupStepMeta(
route = route, route = route,
title = "Auto Patcher", title = translation["setup.activity.patch_title"],
subtitle = "Streamlined download, patch, and install with a single flow.", subtitle = translation["setup.activity.patch_subtitle"],
icon = Icons.Filled.Download icon = Icons.Filled.Download
) )
is RootInstallSnapchatScreen -> SetupStepMeta( is RootInstallSnapchatScreen -> SetupStepMeta(
route = route, route = route,
title = "Snapchat Installer", title = translation["setup.activity.root_install_title"],
subtitle = "Download and install the recommended Snapchat build.", subtitle = translation["setup.activity.root_install_subtitle"],
icon = Icons.Filled.Download icon = Icons.Filled.Download
) )
is SaveFolderScreen -> SetupStepMeta( is SaveFolderScreen -> SetupStepMeta(
route = route, route = route,
title = translation["setup.dialogs.save_folder"] ?: "Where should we save?", title = translation["setup.dialogs.save_folder"],
subtitle = "Pick your personal vault so snaps land exactly where you expect.", subtitle = translation["setup.activity.save_folder_subtitle"],
icon = Icons.Filled.Folder icon = Icons.Filled.Folder
) )
is MappingsScreen -> SetupStepMeta( is MappingsScreen -> SetupStepMeta(
route = route, route = route,
title = translation["setup.mappings.dialog"] ?: "Mapping your Snapchat", title = translation["setup.mappings.dialog"],
subtitle = "We calibrate everything to your install so the magic works flawlessly.", subtitle = translation["setup.activity.mappings_subtitle"],
icon = Icons.Filled.AutoAwesome icon = Icons.Filled.AutoAwesome
) )
@@ -648,7 +656,11 @@ private fun SetupHeader(
tint = Color.White tint = Color.White
) )
Text( Text(
text = "Step ${currentIndex + 1} of $total", text = translation.format(
"setup.activity.step_counter",
"current" to (currentIndex + 1).toString(),
"total" to total.toString()
),
color = Color.White, color = Color.White,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 13.sp fontSize = 13.sp
@@ -723,6 +735,7 @@ private fun StepBadgesRow(steps: List<SetupStepMeta>, currentStep: Int) {
@Composable @Composable
private fun StepBadge(step: SetupStepMeta, state: StepState) { private fun StepBadge(step: SetupStepMeta, state: StepState) {
val translation = SharedContextHolder.remote(LocalContext.current).translation
val baseColor = when (state) { val baseColor = when (state) {
StepState.COMPLETE -> PurrfectPalette.glowSecondary StepState.COMPLETE -> PurrfectPalette.glowSecondary
StepState.ACTIVE -> PurrfectPalette.glowPrimary StepState.ACTIVE -> PurrfectPalette.glowPrimary
@@ -774,9 +787,9 @@ private fun StepBadge(step: SetupStepMeta, state: StepState) {
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
val hint = when (state) { val hint = when (state) {
StepState.COMPLETE -> "Checked off" StepState.COMPLETE -> translation["setup.activity.step_complete"]
StepState.ACTIVE -> "In progress" StepState.ACTIVE -> translation["setup.activity.step_active"]
StepState.UPCOMING -> "Ready next" StepState.UPCOMING -> translation["setup.activity.step_upcoming"]
} }
Text( Text(
text = hint, text = hint,
@@ -868,6 +881,7 @@ private fun NextButton(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val translation = SharedContextHolder.remote(LocalContext.current).translation
val alpha by animateFloatAsState(targetValue = if (enabled) 1f else 0.6f, label = "NextButtonAlpha") val alpha by animateFloatAsState(targetValue = if (enabled) 1f else 0.6f, label = "NextButtonAlpha")
val gradient = Brush.horizontalGradient( val gradient = Brush.horizontalGradient(
listOf( listOf(
@@ -903,7 +917,11 @@ private fun NextButton(
horizontalArrangement = Arrangement.spacedBy(10.dp) horizontalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text( Text(
text = if (isFinalStep) "Finish setup" else "Continue", text = if (isFinalStep) {
translation["setup.activity.finish_button"]
} else {
translation["setup.activity.continue_button"]
},
color = Color.White, color = Color.White,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
fontSize = 16.sp fontSize = 16.sp

View File

@@ -98,10 +98,14 @@ class InstallModeScreen(
} }
if (showGuides) { if (showGuides) {
val confirmLabel = if (timeout > 0) "I understand (${timeout}s)" else "I understand" val confirmLabel = if (timeout > 0) {
context.translation.format("setup.install_mode.confirm_timeout", "seconds" to timeout.toString())
} else {
context.translation["setup.install_mode.confirm"]
}
AestheticDialog( AestheticDialog(
onDismissRequest = { if (timeout == 0) showGuides = false }, onDismissRequest = { if (timeout == 0) showGuides = false },
title = "Please note!", title = context.translation["setup.install_mode.notice_title"],
text = "", text = "",
icon = Icons.Filled.Warning, icon = Icons.Filled.Warning,
confirmButtonText = confirmLabel, confirmButtonText = confirmLabel,
@@ -138,39 +142,39 @@ class InstallModeScreen(
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text( Text(
text = "Select the type of device you have: rooted or non-rooted. If you are unsure, choose Non-root and continue.", text = context.translation["setup.install_mode.notice_intro"],
style = bodyStyle, style = bodyStyle,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Text( Text(
text = "Non-rooted devices", text = context.translation["setup.install_mode.notice_non_root_title"],
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White, color = Color.White,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Start textAlign = TextAlign.Start
) )
Text( Text(
text = "Select Non-root and the app will handle everything. Tap Install Patched Snapchat when it appears. After it installs, do not open Snapchat yet. Continue the PurrfectSnap setup; once it finishes, you can open Snapchat and enjoy.", text = context.translation["setup.install_mode.notice_non_root_body"],
style = bodyStyle, style = bodyStyle,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Text( Text(
text = "Rooted devices", text = context.translation["setup.install_mode.notice_root_title"],
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White, color = Color.White,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Start textAlign = TextAlign.Start
) )
Text( Text(
text = "Make sure you have flashed LSPosed first. We recommend JingMatrix LSPosed or LSPosed Irena. After you select Root, the app will install the recommended Snapchat version. Do not open it yet; continue the PurrfectSnap setup. When setup finishes, enable PurrfectSnap in LSPosed and reboot your phone. Then start using Snapchat. We highly recommend detaching Snapchat from the Play Store with the Zygisk Detach module to prevent auto-updates.", text = context.translation["setup.install_mode.notice_root_body"],
style = bodyStyle, style = bodyStyle,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Text( Text(
text = "If you run into any installation issues, the solution will appear here. Please read it carefully.", text = context.translation["setup.install_mode.notice_issues_hint"],
style = bodyStyle, style = bodyStyle,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -178,9 +182,9 @@ class InstallModeScreen(
Text( Text(
text = buildAnnotatedString { text = buildAnnotatedString {
withStyle(SpanStyle(fontWeight = FontWeight.Bold, color = Color.White)) { withStyle(SpanStyle(fontWeight = FontWeight.Bold, color = Color.White)) {
append("Note: ") append(context.translation["setup.install_mode.notice_note_prefix"])
} }
append("New Accounts easily get locked! It is recommended to use an older account with PurrfectSnap.") append(context.translation["setup.install_mode.notice_note_body"])
}, },
style = bodyStyle, style = bodyStyle,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
@@ -194,8 +198,8 @@ class InstallModeScreen(
SetupCard { SetupCard {
StepTitle( StepTitle(
title = "Choose your device", title = context.translation["setup.install_mode.step_title"],
subtitle = "If you don't know, select Non-rooted device and proceed.", subtitle = context.translation["setup.install_mode.step_subtitle"],
modifier = Modifier.align(Alignment.CenterHorizontally) modifier = Modifier.align(Alignment.CenterHorizontally)
) )
Column( Column(
@@ -203,8 +207,8 @@ class InstallModeScreen(
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
ModeOption( ModeOption(
title = "Rooted device", title = context.translation["setup.install_mode.root_option_title"],
subtitle = "Use Lsposed and skip auto patching.", subtitle = context.translation["setup.install_mode.root_option_subtitle"],
icon = Icons.Filled.VerifiedUser, icon = Icons.Filled.VerifiedUser,
accent = Brush.horizontalGradient( accent = Brush.horizontalGradient(
listOf( listOf(
@@ -219,8 +223,8 @@ class InstallModeScreen(
} }
) )
ModeOption( ModeOption(
title = "Non-rooted device", title = context.translation["setup.install_mode.non_root_option_title"],
subtitle = "Use included auto patcher to install patched Snapchat.", subtitle = context.translation["setup.install_mode.non_root_option_subtitle"],
icon = Icons.Filled.Shield, icon = Icons.Filled.Shield,
accent = Brush.horizontalGradient( accent = Brush.horizontalGradient(
listOf( listOf(
@@ -262,7 +266,7 @@ class InstallModeScreen(
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( Text(
text = "Skip Auto Setup", text = context.translation["setup.install_mode.skip_auto_setup"],
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White, color = Color.White,

View File

@@ -52,6 +52,7 @@ class MappingsScreen : SetupScreen() {
@Composable @Composable
override fun Content() { override fun Content() {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val translation = context.translation
var infoText by remember { mutableStateOf(null as String?) } var infoText by remember { mutableStateOf(null as String?) }
var isGenerating by remember { mutableStateOf(false) } var isGenerating by remember { mutableStateOf(false) }
var showCompletionNotice by remember { mutableStateOf(false) } var showCompletionNotice by remember { mutableStateOf(false) }
@@ -98,13 +99,16 @@ class MappingsScreen : SetupScreen() {
if (showCompletionNotice) { if (showCompletionNotice) {
val confirmLabel = if (completionCountdown > 0) { val confirmLabel = if (completionCountdown > 0) {
"I understand (${completionCountdown}s)" translation.format(
"setup.mappings.confirm_understand_timeout",
"seconds" to completionCountdown.toString()
)
} else { } else {
"I understand" translation["setup.mappings.confirm_understand"]
} }
AestheticDialog( AestheticDialog(
onDismissRequest = { if (completionCountdown == 0) { showCompletionNotice = false; goNext() } }, onDismissRequest = { if (completionCountdown == 0) { showCompletionNotice = false; goNext() } },
title = "Please note!", title = translation["setup.mappings.notice_title"],
text = "", text = "",
icon = Icons.Filled.Warning, icon = Icons.Filled.Warning,
confirmButtonText = confirmLabel, confirmButtonText = confirmLabel,
@@ -141,27 +145,27 @@ class MappingsScreen : SetupScreen() {
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text( Text(
text = "If you see the \"Account temporarily disabled\" error while logging in, do not worry. Follow these steps in order:", text = translation["setup.mappings.notice_intro"],
style = bodyStyle, style = bodyStyle,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Text( Text(
text = "1. Reopen Snapchat and log in. This fixes it most of the time.", text = translation["setup.mappings.notice_step_1"],
style = bodyStyle, style = bodyStyle,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Text( Text(
text = "2. If it still fails, tap the login button repeatedly. This usually covers the next chunk.", text = translation["setup.mappings.notice_step_2"],
style = bodyStyle, style = bodyStyle,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Text( Text(
text = "3. If it still fails, clear Snapchat's data, disable any VPN, and log in again.", text = translation["setup.mappings.notice_step_3"],
style = bodyStyle, style = bodyStyle,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Text( Text(
text = "For rooted users:", text = translation["setup.mappings.notice_rooted_title"],
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.bodyMedium.copy(
color = Color.White, color = Color.White,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
@@ -170,7 +174,7 @@ class MappingsScreen : SetupScreen() {
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Text( Text(
text = "Reopen Snapchat and log in. If it still fails, disable PurrfectSnap in LSPosed, log in, then re-enable PurrfectSnap.", text = translation["setup.mappings.notice_rooted_body"],
style = bodyStyle, style = bodyStyle,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
@@ -193,7 +197,11 @@ class MappingsScreen : SetupScreen() {
if (warnings.isNotEmpty()) { if (warnings.isNotEmpty()) {
isGenerating = false isGenerating = false
infoText = "${warnings.size} warning(s) occurred while generating mappings:\n\n${warnings.joinToString("\n")}".also { infoText = translation.format(
"setup.mappings.warnings_info",
"count" to warnings.size.toString(),
"warnings" to warnings.joinToString("\n")
).also {
context.log.warn(it) context.log.warn(it)
} }
return@launch return@launch
@@ -216,7 +224,7 @@ class MappingsScreen : SetupScreen() {
subtitle = null subtitle = null
) )
DialogText( DialogText(
text = "This only takes a moment. Keep the app open while magic happens!" text = translation["setup.mappings.progress_hint"]
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Surface( Surface(

View File

@@ -71,6 +71,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.eternal.purrfectsnap.common.bridge.wrapper.LocaleWrapper
import me.eternal.purrfectsnap.setup.patch.AutoPatchServer import me.eternal.purrfectsnap.setup.patch.AutoPatchServer
import me.eternal.purrfectsnap.setup.patch.LSPatch import me.eternal.purrfectsnap.setup.patch.LSPatch
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
@@ -93,7 +94,8 @@ class PatchSnapchatScreen : SetupScreen() {
@Composable @Composable
override fun Content() { override fun Content() {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val logs = remember { mutableStateListOf("Auto Patcher is ready.") } val translation = context.translation
val logs = remember { mutableStateListOf(translation["setup.patch.ready_log"]) }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
var progress by remember { mutableFloatStateOf(-1f) } var progress by remember { mutableFloatStateOf(-1f) }
@@ -146,7 +148,7 @@ class PatchSnapchatScreen : SetupScreen() {
repeat(80) { repeat(80) {
if (isSnapchatInstalledAfter(patchStartedAt)) { if (isSnapchatInstalledAfter(patchStartedAt)) {
installVerified = true installVerified = true
pushLog("Snapchat install confirmed. You're cleared to continue.") pushLog(translation["setup.patch.install_confirmed_log"])
return@launch return@launch
} }
delay(1200) delay(1200)
@@ -179,7 +181,12 @@ class PatchSnapchatScreen : SetupScreen() {
suspend fun downloadSnapchatFromAutoPatchServer(): File? = withContext(Dispatchers.IO) { suspend fun downloadSnapchatFromAutoPatchServer(): File? = withContext(Dispatchers.IO) {
val latestApk = autoPatchServer.fetchLatestSnapchatApk() ?: return@withContext null val latestApk = autoPatchServer.fetchLatestSnapchatApk() ?: return@withContext null
pushStatus("Downloading recommended Snapchat version (${latestApk.tagName})...") pushStatus(
translation.format(
"setup.patch.download_recommended_status",
"version" to latestApk.tagName
)
)
okHttpClient.newCall(Request.Builder().url(latestApk.downloadUrl).build()).execute().use { response -> okHttpClient.newCall(Request.Builder().url(latestApk.downloadUrl).build()).execute().use { response ->
if (!response.isSuccessful) return@withContext null if (!response.isSuccessful) return@withContext null
@@ -223,22 +230,27 @@ class PatchSnapchatScreen : SetupScreen() {
downloadFinished = false downloadFinished = false
patchStartedAt = System.currentTimeMillis() patchStartedAt = System.currentTimeMillis()
logs.clear() logs.clear()
pushLog("Starting Auto Patcher for recommended Snapchat version.") pushLog(translation["setup.patch.starting_log"])
runCatching { runCatching {
if (isSnapchatInstalled()) { if (isSnapchatInstalled()) {
pushStatus("Snapchat is installed. Please uninstall it first (don't keep data), then start Auto Patcher again.") pushStatus(translation["setup.patch.uninstall_prompt_status"])
throw IllegalStateException("Snapchat still installed. Uninstall it first, to continue.") throw IllegalStateException(translation["setup.patch.uninstall_prompt_error"])
} }
val modulePath = context.androidContext.packageManager.getPackageInfo( val modulePath = context.androidContext.packageManager.getPackageInfo(
context.androidContext.packageName, 0 context.androidContext.packageName, 0
).applicationInfo?.sourceDir ?: throw IllegalStateException("Module apk not found") ).applicationInfo?.sourceDir ?: throw IllegalStateException(translation["setup.patch.module_apk_not_found_error"])
pushStatus("Fetching recommended Snapchat APK...") pushStatus(translation["setup.patch.fetching_apk_status"])
val downloaded = downloadSnapchatFromAutoPatchServer() val downloaded = downloadSnapchatFromAutoPatchServer()
?: throw IllegalStateException("Download failed") ?: throw IllegalStateException(translation["setup.patch.download_failed_error"])
downloadedApkPath = downloaded.absolutePath downloadedApkPath = downloaded.absolutePath
pushStatus("Download completed: ${downloaded.name}") pushStatus(
translation.format(
"setup.patch.download_completed_status",
"fileName" to downloaded.name
)
)
downloadFinished = true downloadFinished = true
pushStatus("Starting patch powered by Jingmatrix Lspatch") pushStatus(translation["setup.patch.starting_patch_status"])
val lsPatch = LSPatch( val lsPatch = LSPatch(
context.androidContext, context.androidContext,
mapOf(context.androidContext.packageName to File(modulePath)), mapOf(context.androidContext.packageName to File(modulePath)),
@@ -247,16 +259,22 @@ class PatchSnapchatScreen : SetupScreen() {
) )
val outputs = withContext(Dispatchers.IO) { lsPatch.patchSplits(listOf(downloaded)) } val outputs = withContext(Dispatchers.IO) { lsPatch.patchSplits(listOf(downloaded)) }
val patched = outputs["base.apk"] ?: outputs.values.firstOrNull() val patched = outputs["base.apk"] ?: outputs.values.firstOrNull()
?: throw IllegalStateException("Patched apk not produced") ?: throw IllegalStateException(translation["setup.patch.patched_not_produced_error"])
patchedApkPath = patched.absolutePath patchedApkPath = patched.absolutePath
pushStatus("Patched build ready. Install to finish.") pushStatus(translation["setup.patch.patched_ready_status"])
}.onFailure { }.onFailure {
val message = it.message ?: it.toString()
error = it.message ?: it.toString() error = it.message ?: it.toString()
it.stackTraceToString() it.stackTraceToString()
.lineSequence() .lineSequence()
.filter { line -> line.isNotBlank() } .filter { line -> line.isNotBlank() }
.forEach { line -> pushLog(line) } .forEach { line -> pushLog(line) }
pushStatus("Failed: ${it.message}") pushStatus(
translation.format(
"setup.patch.failed_status",
"message" to message
)
)
} }
isRunning = false isRunning = false
progress = -1f progress = -1f
@@ -282,7 +300,7 @@ class PatchSnapchatScreen : SetupScreen() {
fun markAlreadyInstalled() { fun markAlreadyInstalled() {
installRequested = false installRequested = false
installVerified = true installVerified = true
pushLog("Marked as installed manually. You're cleared to continue.") pushLog(translation["setup.patch.mark_installed_log"])
} }
val accent = remember { val accent = remember {
@@ -297,10 +315,10 @@ class PatchSnapchatScreen : SetupScreen() {
if (showIssuesDialog) { if (showIssuesDialog) {
AestheticDialog( AestheticDialog(
onDismissRequest = { showIssuesDialog = false }, onDismissRequest = { showIssuesDialog = false },
title = "Facing issues?", title = translation["setup.patch.issues_title"],
text = "", text = "",
icon = Icons.Filled.Info, icon = Icons.Filled.Info,
confirmButtonText = "Got it", confirmButtonText = translation["setup.patch.issues_confirm"],
onConfirm = { showIssuesDialog = false }, onConfirm = { showIssuesDialog = false },
showCloseButton = false, showCloseButton = false,
customContent = { customContent = {
@@ -316,36 +334,36 @@ class PatchSnapchatScreen : SetupScreen() {
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text( Text(
text = "How to fix installation errors", text = translation["setup.patch.issues_heading"],
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White, color = Color.White,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Text( Text(
text = "Issue: App cannot be installed because it conflicts with an existing package.", text = translation["setup.patch.issues_conflict_issue"],
style = bodyStyle, style = bodyStyle,
textAlign = TextAlign.Start textAlign = TextAlign.Start
) )
Text( Text(
text = "Fix: Download Snapchat from the Play Store and uninstall it without keeping data. Run Auto Patcher again. If it still does not work, run:", text = translation["setup.patch.issues_conflict_fix"],
style = bodyStyle, style = bodyStyle,
textAlign = TextAlign.Start textAlign = TextAlign.Start
) )
Text( Text(
text = "adb uninstall com.snapchat.android", text = translation["setup.patch.issues_adb_command"],
style = bodyStyle, style = bodyStyle,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
softWrap = false, softWrap = false,
modifier = Modifier.horizontalScroll(rememberScrollState()) modifier = Modifier.horizontalScroll(rememberScrollState())
) )
Text( Text(
text = "Issue: App not installed because the package appears to be invalid.", text = translation["setup.patch.issues_invalid_issue"],
style = bodyStyle, style = bodyStyle,
textAlign = TextAlign.Start textAlign = TextAlign.Start
) )
Text( Text(
text = "Fix: Download and install JingMatrix LSPatch, then patch a Snapchat version (any one) from this range, i.e. between 13.65.1.0 and 13.71.0.51, in Integrated mode. Select Embed Modules and embed the PurrfectSnap APK. Then choose Skip auto setup during PurrfectSnap setup to skip Auto Patcher.", text = translation["setup.patch.issues_invalid_fix"],
style = bodyStyle, style = bodyStyle,
textAlign = TextAlign.Start textAlign = TextAlign.Start
) )
@@ -356,12 +374,12 @@ class PatchSnapchatScreen : SetupScreen() {
SetupCard { SetupCard {
StepTitle( StepTitle(
title = "Auto Patcher", title = translation["setup.patch.title"],
subtitle = null, subtitle = null,
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
JingmatrixBadge(accent) JingmatrixBadge(accent, translation)
Surface( Surface(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp), shape = RoundedCornerShape(24.dp),
@@ -392,9 +410,12 @@ class PatchSnapchatScreen : SetupScreen() {
val isPatching = isRunning && downloadFinished && !isDownloading val isPatching = isRunning && downloadFinished && !isDownloading
Text( Text(
text = when { text = when {
isDownloading -> "Downloading Snapchat ${(progress * 100).toInt()}%" isDownloading -> translation.format(
isPatching -> "Patching..." "setup.patch.status_downloading",
else -> "Initializing..." "percent" to (progress * 100).toInt().toString()
)
isPatching -> translation["setup.patch.status_patching"]
else -> translation["setup.patch.status_initializing"]
}, },
color = PurrfectPalette.textPrimary, color = PurrfectPalette.textPrimary,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
@@ -426,9 +447,10 @@ class PatchSnapchatScreen : SetupScreen() {
logs = logs, logs = logs,
pulse = logPulse, pulse = logPulse,
accent = accent, accent = accent,
translation = translation,
onCopy = { onCopy = {
clipboard.setText(AnnotatedString(logs.joinToString("\n"))) clipboard.setText(AnnotatedString(logs.joinToString("\n")))
pushLog("Logs copied to clipboard.") pushLog(translation["setup.patch.logs_copied"])
} }
) )
@@ -463,7 +485,7 @@ class PatchSnapchatScreen : SetupScreen() {
tint = Color.White tint = Color.White
) )
Text( Text(
text = "Patched APK installed", text = translation["setup.patch.install_success"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
@@ -472,7 +494,7 @@ class PatchSnapchatScreen : SetupScreen() {
} else { } else {
if (patchedApk == null) { if (patchedApk == null) {
GradientActionButton( GradientActionButton(
label = "Start auto patch", label = translation["setup.patch.start_button"],
icon = Icons.Filled.Download, icon = Icons.Filled.Download,
onClick = { startPatch() }, onClick = { startPatch() },
enabled = !isRunning enabled = !isRunning
@@ -480,7 +502,7 @@ class PatchSnapchatScreen : SetupScreen() {
} }
if (patchedApk != null) { if (patchedApk != null) {
GradientActionButton( GradientActionButton(
label = "Install patched Snapchat", label = translation["setup.patch.install_button"],
icon = Icons.Filled.Verified, icon = Icons.Filled.Verified,
onClick = { installPatchedApk() }, onClick = { installPatchedApk() },
enabled = true enabled = true
@@ -509,7 +531,7 @@ class PatchSnapchatScreen : SetupScreen() {
tint = Color.White.copy(alpha = 0.9f) tint = Color.White.copy(alpha = 0.9f)
) )
Text( Text(
text = "Facing issues?", text = translation["setup.patch.issues_title"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
@@ -539,7 +561,7 @@ class PatchSnapchatScreen : SetupScreen() {
tint = Color.White.copy(alpha = 0.9f) tint = Color.White.copy(alpha = 0.9f)
) )
Text( Text(
text = "Already Installed?", text = translation["setup.patch.already_installed_button"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
@@ -555,7 +577,10 @@ class PatchSnapchatScreen : SetupScreen() {
} }
@Composable @Composable
private fun JingmatrixBadge(accent: Brush) { private fun JingmatrixBadge(
accent: Brush,
translation: LocaleWrapper
) {
Surface( Surface(
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
color = Color.White.copy(alpha = 0.06f), color = Color.White.copy(alpha = 0.06f),
@@ -581,7 +606,7 @@ private fun JingmatrixBadge(accent: Brush) {
) )
} }
Text( Text(
text = "Powered by Jingmatrix Lspatch", text = translation["setup.patch.powered_by_label"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
@@ -647,6 +672,7 @@ private fun LogsPanel(
logs: List<String>, logs: List<String>,
pulse: Float, pulse: Float,
accent: Brush, accent: Brush,
translation: LocaleWrapper,
onCopy: () -> Unit onCopy: () -> Unit
) { ) {
var expanded by rememberSaveable { mutableStateOf(false) } var expanded by rememberSaveable { mutableStateOf(false) }
@@ -685,7 +711,7 @@ private fun LogsPanel(
horizontalArrangement = Arrangement.spacedBy(6.dp) horizontalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
Text( Text(
text = "Logs", text = translation["setup.patch.logs_title"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
@@ -715,7 +741,7 @@ private fun LogsPanel(
modifier = Modifier.size(14.dp) modifier = Modifier.size(14.dp)
) )
Text( Text(
text = "Copy", text = translation["setup.patch.copy_button"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 12.sp fontSize = 12.sp
@@ -727,7 +753,10 @@ private fun LogsPanel(
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
logs.forEach { line -> logs.forEach { line ->
Text( Text(
text = "- $line", text = translation.format(
"setup.patch.log_line_prefix",
"line" to line
),
color = PurrfectPalette.textPrimary, color = PurrfectPalette.textPrimary,
fontSize = 13.sp, fontSize = 13.sp,
lineHeight = 16.sp lineHeight = 16.sp

View File

@@ -73,9 +73,9 @@ class PermissionsScreen : SetupScreen() {
private fun descriptionFor(key: String): String { private fun descriptionFor(key: String): String {
return when (key) { return when (key) {
"notification_access" -> "Alerts you the second downloads finish." "notification_access" -> context.translation["setup.permissions.notification_access_description"]
"battery_optimization" -> "Keeps background tasks alive without being killed." "battery_optimization" -> context.translation["setup.permissions.battery_optimization_description"]
"display_over_other_apps" -> "Enables floating overlays while you are in Snapchat." "display_over_other_apps" -> context.translation["setup.permissions.display_over_other_apps_description"]
else -> "" else -> ""
} }
} }
@@ -178,7 +178,7 @@ class PermissionsScreen : SetupScreen() {
modifier = Modifier.padding(end = 6.dp) modifier = Modifier.padding(end = 6.dp)
) )
Text( Text(
text = "Granted", text = context.translation["setup.permissions.granted_label"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )

View File

@@ -151,7 +151,7 @@ class PickLanguageScreen : SetupScreen() {
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text( Text(
text = "Current selection", text = context.translation["setup.pick_language.current_selection"],
fontSize = 14.sp, fontSize = 14.sp,
color = PurrfectPalette.textSecondary color = PurrfectPalette.textSecondary
) )
@@ -180,9 +180,9 @@ class PickLanguageScreen : SetupScreen() {
contentColor = Color.White contentColor = Color.White
) )
) { ) {
Text(text = "Browse languages") Text(text = context.translation["setup.pick_language.browse_languages"])
} }
DialogText(text = "You can change this anytime from PurrfectSnap settings.") DialogText(text = context.translation["setup.pick_language.change_anytime_hint"])
} }
if (isDialog) { if (isDialog) {
@@ -196,7 +196,7 @@ class PickLanguageScreen : SetupScreen() {
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
StepTitle( StepTitle(
title = "Available Languages", title = context.translation["setup.pick_language.available_languages"],
subtitle = null, subtitle = null,
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center textAlign = TextAlign.Center

View File

@@ -66,6 +66,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.eternal.purrfectsnap.common.bridge.wrapper.LocaleWrapper
import me.eternal.purrfectsnap.setup.patch.AutoPatchServer import me.eternal.purrfectsnap.setup.patch.AutoPatchServer
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
import me.eternal.purrfectsnap.ui.setup.screens.SetupScreen import me.eternal.purrfectsnap.ui.setup.screens.SetupScreen
@@ -86,7 +87,8 @@ class RootInstallSnapchatScreen : SetupScreen() {
@Composable @Composable
override fun Content() { override fun Content() {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val logs = remember { mutableStateListOf("Snapchat installer is ready.") } val translation = context.translation
val logs = remember { mutableStateListOf(translation["setup.root_install.ready_log"]) }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
var progress by remember { mutableFloatStateOf(-1f) } var progress by remember { mutableFloatStateOf(-1f) }
@@ -136,7 +138,7 @@ class RootInstallSnapchatScreen : SetupScreen() {
repeat(80) { repeat(80) {
if (isSnapchatInstalledAfter(downloadStartedAt)) { if (isSnapchatInstalledAfter(downloadStartedAt)) {
installVerified = true installVerified = true
pushLog("Snapchat install confirmed. You're cleared to continue.") pushLog(translation["setup.root_install.install_confirmed_log"])
return@launch return@launch
} }
delay(1200) delay(1200)
@@ -168,7 +170,12 @@ class RootInstallSnapchatScreen : SetupScreen() {
suspend fun downloadSnapchatFromAutoPatchServer(): File? = withContext(Dispatchers.IO) { suspend fun downloadSnapchatFromAutoPatchServer(): File? = withContext(Dispatchers.IO) {
val latestApk = autoPatchServer.fetchLatestSnapchatApk() ?: return@withContext null val latestApk = autoPatchServer.fetchLatestSnapchatApk() ?: return@withContext null
pushStatus("Downloading recommended Snapchat version (${latestApk.tagName})...") pushStatus(
translation.format(
"setup.root_install.download_recommended_status",
"version" to latestApk.tagName
)
)
okHttpClient.newCall(Request.Builder().url(latestApk.downloadUrl).build()).execute().use { response -> okHttpClient.newCall(Request.Builder().url(latestApk.downloadUrl).build()).execute().use { response ->
if (!response.isSuccessful) return@withContext null if (!response.isSuccessful) return@withContext null
@@ -218,7 +225,7 @@ class RootInstallSnapchatScreen : SetupScreen() {
fun markAlreadyInstalled() { fun markAlreadyInstalled() {
installRequested = false installRequested = false
installVerified = true installVerified = true
pushLog("Marked as installed manually. You're cleared to continue.") pushLog(translation["setup.root_install.mark_installed_log"])
} }
fun startDownloadAndInstall() { fun startDownloadAndInstall() {
@@ -233,27 +240,38 @@ class RootInstallSnapchatScreen : SetupScreen() {
downloadFinished = false downloadFinished = false
downloadStartedAt = System.currentTimeMillis() downloadStartedAt = System.currentTimeMillis()
logs.clear() logs.clear()
pushLog("Starting Snapchat download for rooted install.") pushLog(translation["setup.root_install.start_download_log"])
runCatching { runCatching {
if (isSnapchatInstalled()) { if (isSnapchatInstalled()) {
pushStatus("Snapchat is installed. Please uninstall it first (don't keep data), then try again.") pushStatus(translation["setup.root_install.uninstall_prompt_status"])
throw IllegalStateException("Snapchat still installed. Uninstall it first, to continue.") throw IllegalStateException(translation["setup.root_install.uninstall_prompt_error"])
} }
pushStatus("Fetching recommended Snapchat APK...") pushStatus(translation["setup.root_install.fetching_apk_status"])
val downloaded = downloadSnapchatFromAutoPatchServer() val downloaded = downloadSnapchatFromAutoPatchServer()
?: throw IllegalStateException("Download failed") ?: throw IllegalStateException(translation["setup.root_install.download_failed_error"])
downloadedApkPath = downloaded.absolutePath downloadedApkPath = downloaded.absolutePath
pushStatus("Download completed: ${downloaded.name}") pushStatus(
translation.format(
"setup.root_install.download_completed_status",
"fileName" to downloaded.name
)
)
downloadFinished = true downloadFinished = true
pushStatus("Launching installer...") pushStatus(translation["setup.root_install.launching_installer_status"])
installDownloadedApk() installDownloadedApk()
}.onFailure { }.onFailure {
val message = it.message ?: it.toString()
error = it.message ?: it.toString() error = it.message ?: it.toString()
it.stackTraceToString() it.stackTraceToString()
.lineSequence() .lineSequence()
.filter { line -> line.isNotBlank() } .filter { line -> line.isNotBlank() }
.forEach { line -> pushLog(line) } .forEach { line -> pushLog(line) }
pushStatus("Failed: ${it.message}") pushStatus(
translation.format(
"setup.root_install.failed_status",
"message" to message
)
)
} }
isRunning = false isRunning = false
progress = -1f progress = -1f
@@ -271,7 +289,7 @@ class RootInstallSnapchatScreen : SetupScreen() {
SetupCard { SetupCard {
StepTitle( StepTitle(
title = "Snapchat Installer", title = translation["setup.root_install.title"],
subtitle = null, subtitle = null,
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center textAlign = TextAlign.Center
@@ -305,9 +323,12 @@ class RootInstallSnapchatScreen : SetupScreen() {
val isDownloading = progress >= 0f val isDownloading = progress >= 0f
Text( Text(
text = if (isDownloading) { text = if (isDownloading) {
"Downloading Snapchat ${(progress * 100).toInt()}%" translation.format(
"setup.root_install.status_downloading",
"percent" to (progress * 100).toInt().toString()
)
} else { } else {
"Preparing installer..." translation["setup.root_install.status_preparing"]
}, },
color = PurrfectPalette.textPrimary, color = PurrfectPalette.textPrimary,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
@@ -339,9 +360,10 @@ class RootInstallSnapchatScreen : SetupScreen() {
logs = logs, logs = logs,
pulse = logPulse, pulse = logPulse,
accent = accent, accent = accent,
translation = translation,
onCopy = { onCopy = {
clipboard.setText(AnnotatedString(logs.joinToString("\n"))) clipboard.setText(AnnotatedString(logs.joinToString("\n")))
pushLog("Logs copied to clipboard.") pushLog(translation["setup.root_install.logs_copied"])
} }
) )
@@ -376,7 +398,7 @@ class RootInstallSnapchatScreen : SetupScreen() {
tint = Color.White tint = Color.White
) )
Text( Text(
text = "Snapchat installed", text = translation["setup.root_install.install_success"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
@@ -385,7 +407,7 @@ class RootInstallSnapchatScreen : SetupScreen() {
} else { } else {
if (downloadedApk == null) { if (downloadedApk == null) {
GradientActionButton( GradientActionButton(
label = "Download Snapchat", label = translation["setup.root_install.download_button"],
icon = Icons.Filled.Download, icon = Icons.Filled.Download,
onClick = { startDownloadAndInstall() }, onClick = { startDownloadAndInstall() },
enabled = !isRunning enabled = !isRunning
@@ -393,7 +415,7 @@ class RootInstallSnapchatScreen : SetupScreen() {
} }
if (downloadedApk != null) { if (downloadedApk != null) {
GradientActionButton( GradientActionButton(
label = "Install Snapchat", label = translation["setup.root_install.install_button"],
icon = Icons.Filled.Verified, icon = Icons.Filled.Verified,
onClick = { installDownloadedApk() }, onClick = { installDownloadedApk() },
enabled = true enabled = true
@@ -422,7 +444,7 @@ class RootInstallSnapchatScreen : SetupScreen() {
tint = Color.White.copy(alpha = 0.9f) tint = Color.White.copy(alpha = 0.9f)
) )
Text( Text(
text = "Already Installed?", text = translation["setup.root_install.already_installed_button"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
@@ -495,6 +517,7 @@ private fun LogsPanel(
logs: List<String>, logs: List<String>,
pulse: Float, pulse: Float,
accent: Brush, accent: Brush,
translation: LocaleWrapper,
onCopy: () -> Unit onCopy: () -> Unit
) { ) {
var expanded by rememberSaveable { mutableStateOf(false) } var expanded by rememberSaveable { mutableStateOf(false) }
@@ -533,7 +556,7 @@ private fun LogsPanel(
horizontalArrangement = Arrangement.spacedBy(6.dp) horizontalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
Text( Text(
text = "Logs", text = translation["setup.root_install.logs_title"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
@@ -563,7 +586,7 @@ private fun LogsPanel(
modifier = Modifier.size(14.dp) modifier = Modifier.size(14.dp)
) )
Text( Text(
text = "Copy", text = translation["setup.root_install.copy_button"],
color = Color.White, color = Color.White,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 12.sp fontSize = 12.sp
@@ -575,7 +598,10 @@ private fun LogsPanel(
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
logs.forEach { line -> logs.forEach { line ->
Text( Text(
text = "- $line", text = translation.format(
"setup.root_install.log_line_prefix",
"line" to line
),
color = PurrfectPalette.textPrimary, color = PurrfectPalette.textPrimary,
fontSize = 13.sp, fontSize = 13.sp,
lineHeight = 16.sp lineHeight = 16.sp

View File

@@ -58,7 +58,7 @@ class SaveFolderScreen : SetupScreen() {
title = context.translation["setup.dialogs.save_folder"], title = context.translation["setup.dialogs.save_folder"],
subtitle = null subtitle = null
) )
DialogText(text = "Please choose the location where media should be downloaded to.") DialogText(text = context.translation["setup.save_folder.description"])
Surface( Surface(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(22.dp), shape = RoundedCornerShape(22.dp),
@@ -99,12 +99,12 @@ class SaveFolderScreen : SetupScreen() {
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text( Text(
text = "Destination", text = context.translation["setup.save_folder.destination_label"],
fontSize = 13.sp, fontSize = 13.sp,
color = PurrfectPalette.textSecondary color = PurrfectPalette.textSecondary
) )
Text( Text(
text = if (currentFolder.isBlank()) "System default" else currentFolder, text = if (currentFolder.isBlank()) context.translation["setup.save_folder.system_default_label"] else currentFolder,
fontSize = 15.sp, fontSize = 15.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White, color = Color.White,
@@ -162,7 +162,7 @@ class SaveFolderScreen : SetupScreen() {
), ),
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.18f)) border = BorderStroke(1.dp, Color.White.copy(alpha = 0.18f))
) { ) {
Text(text = "Use default location") Text(text = context.translation["setup.save_folder.use_default_location_button"])
} }
if (showNoPickerDialog) { if (showNoPickerDialog) {
@@ -204,13 +204,13 @@ class SaveFolderScreen : SetupScreen() {
} }
} }
Text( Text(
text = "Folder picker unavailable", text = context.translation["setup.save_folder.no_picker_title"],
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.ExtraBold, fontWeight = FontWeight.ExtraBold,
color = Color.White color = Color.White
) )
Text( Text(
text = "Some cloned/dual-app environments block the system folder picker. You can continue using the system default save location, or open the app outside clone mode to select a custom folder.", text = context.translation["setup.save_folder.no_picker_message"],
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 18.sp, lineHeight = 18.sp,
textAlign = androidx.compose.ui.text.style.TextAlign.Center, textAlign = androidx.compose.ui.text.style.TextAlign.Center,
@@ -230,7 +230,7 @@ class SaveFolderScreen : SetupScreen() {
), ),
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.14f)) border = BorderStroke(1.dp, Color.White.copy(alpha = 0.14f))
) { ) {
Text("Cancel") Text(context.translation["button.cancel"])
} }
Button( Button(
onClick = { onClick = {
@@ -248,7 +248,7 @@ class SaveFolderScreen : SetupScreen() {
contentColor = Color.White contentColor = Color.White
) )
) { ) {
Text("Use default") Text(context.translation["setup.save_folder.use_default_button"])
} }
} }
} }
@@ -256,7 +256,7 @@ class SaveFolderScreen : SetupScreen() {
} }
} }
DialogText( DialogText(
text = "PurrfectSnap requires Storage permissions to download and Save Media from Snapchat." text = context.translation["setup.save_folder.permission_hint"]
) )
} }
} }

View File

@@ -326,7 +326,7 @@ class AlertDialogs(
Button( Button(
onClick = { onClick = {
if (fieldValue.text.isNotEmpty() && property.key.params.inputCheck?.invoke(fieldValue.text) == false) { if (fieldValue.text.isNotEmpty() && property.key.params.inputCheck?.invoke(fieldValue.text) == false) {
Toast.makeText(context, "Invalid input! Make sure you entered a valid value.", Toast.LENGTH_SHORT).show() //TODO: i18n Toast.makeText(context, translation["invalid_input_toast"], Toast.LENGTH_SHORT).show()
return@Button return@Button
} }

View File

@@ -1,21 +1,160 @@
{ {
"setup": { "setup": {
"activity": {
"wrong_apk_title": "Wrong APK installed",
"wrong_apk_message": "Your device is armv8, please download the armv8 apk, not armv7.",
"close_button": "Close",
"important_confirm_timeout": "I understand ({seconds}s)",
"important_confirm": "I understand",
"important_title": "Important!",
"important_message": "If you have used SnapEnhance or any other mod besides PurrfectSnap, we recommend uninstalling everything and staying on stock Snapchat for one week. Then switch to PurrfectSnap after next Friday.",
"language_subtitle": "Tune PurrfectSnap to speak your voice before anything else.",
"install_mode_title": "Choose your device",
"install_mode_subtitle": "Pick the path that matches how you'll install PurrfectSnap.",
"permissions_subtitle": "Grant the essentials so overlays, downloads, and alerts stay reliable.",
"patch_title": "Auto Patcher",
"patch_subtitle": "Streamlined download, patch, and install with a single flow.",
"root_install_title": "Snapchat Installer",
"root_install_subtitle": "Download and install the recommended Snapchat build.",
"save_folder_subtitle": "Pick your personal vault so snaps land exactly where you expect.",
"mappings_subtitle": "We calibrate everything to your install so the magic works flawlessly.",
"step_counter": "Step {current} of {total}",
"step_complete": "Checked off",
"step_active": "In progress",
"step_upcoming": "Ready next",
"finish_button": "Finish setup",
"continue_button": "Continue"
},
"dialogs": { "dialogs": {
"select_language": "Select Language", "select_language": "Select Language",
"save_folder": "Choose where to save downloads", "save_folder": "Choose where to save downloads",
"select_save_folder_button": "Select Folder" "select_save_folder_button": "Select Folder"
}, },
"install_mode": {
"confirm_timeout": "I understand ({seconds}s)",
"confirm": "I understand",
"notice_title": "Please note!",
"notice_intro": "Select the type of device you have: rooted or non-rooted. If you are unsure, choose Non-root and continue.",
"notice_non_root_title": "Non-rooted devices",
"notice_non_root_body": "Select Non-root and the app will handle everything. Tap Install Patched Snapchat when it appears. After it installs, do not open Snapchat yet. Continue the PurrfectSnap setup; once it finishes, you can open Snapchat and enjoy.",
"notice_root_title": "Rooted devices",
"notice_root_body": "Make sure you have flashed LSPosed first. We recommend JingMatrix LSPosed or LSPosed Irena. After you select Root, the app will install the recommended Snapchat version. Do not open it yet; continue the PurrfectSnap setup. When setup finishes, enable PurrfectSnap in LSPosed and reboot your phone. Then start using Snapchat. We highly recommend detaching Snapchat from the Play Store with the Zygisk Detach module to prevent auto-updates.",
"notice_issues_hint": "If you run into any installation issues, the solution will appear here. Please read it carefully.",
"notice_note_prefix": "Note: ",
"notice_note_body": "New Accounts easily get locked! It is recommended to use an older account with PurrfectSnap.",
"step_title": "Choose your device",
"step_subtitle": "If you don't know, select Non-rooted device and proceed.",
"root_option_title": "Rooted device",
"root_option_subtitle": "Use Lsposed and skip auto patching.",
"non_root_option_title": "Non-rooted device",
"non_root_option_subtitle": "Use included auto patcher to install patched Snapchat.",
"skip_auto_setup": "Skip Auto Setup"
},
"mappings": { "mappings": {
"dialog": "Generating Mappings...", "dialog": "Generating Mappings...",
"generate_failure_no_snapchat": "PurrfectSnap was unable to detect Snapchat, please try reinstalling Snapchat.", "generate_failure_no_snapchat": "PurrfectSnap was unable to detect Snapchat, please try reinstalling Snapchat.",
"generate_failure": "An error occurred while trying to generate mappings, please try again." "generate_failure": "An error occurred while trying to generate mappings, please try again.",
"confirm_understand_timeout": "I understand ({seconds}s)",
"confirm_understand": "I understand",
"notice_title": "Please note!",
"notice_intro": "If you see the \"Account temporarily disabled\" error while logging in, do not worry. Follow these steps in order:",
"notice_step_1": "1. Reopen Snapchat and log in. This fixes it most of the time.",
"notice_step_2": "2. If it still fails, tap the login button repeatedly. This usually covers the next chunk.",
"notice_step_3": "3. If it still fails, clear Snapchat's data, disable any VPN, and log in again.",
"notice_rooted_title": "For rooted users:",
"notice_rooted_body": "Reopen Snapchat and log in. If it still fails, disable PurrfectSnap in LSPosed, log in, then re-enable PurrfectSnap.",
"warnings_info": "{count} warning(s) occurred while generating mappings:\n\n{warnings}",
"progress_hint": "This only takes a moment. Keep the app open while magic happens!"
},
"patch": {
"title": "Auto Patcher",
"ready_log": "Auto Patcher is ready.",
"install_confirmed_log": "Snapchat install confirmed. You're cleared to continue.",
"download_recommended_status": "Downloading recommended Snapchat version ({version})...",
"starting_log": "Starting Auto Patcher for recommended Snapchat version.",
"uninstall_prompt_status": "Snapchat is installed. Please uninstall it first (don't keep data), then start Auto Patcher again.",
"uninstall_prompt_error": "Snapchat still installed. Uninstall it first, to continue.",
"module_apk_not_found_error": "Module apk not found",
"fetching_apk_status": "Fetching recommended Snapchat APK...",
"download_failed_error": "Download failed",
"download_completed_status": "Download completed: {fileName}",
"starting_patch_status": "Starting patch powered by Jingmatrix Lspatch",
"patched_not_produced_error": "Patched apk not produced",
"patched_ready_status": "Patched build ready. Install to finish.",
"failed_status": "Failed: {message}",
"mark_installed_log": "Marked as installed manually. You're cleared to continue.",
"issues_title": "Facing issues?",
"issues_confirm": "Got it",
"issues_heading": "How to fix installation errors",
"issues_conflict_issue": "Issue: App cannot be installed because it conflicts with an existing package.",
"issues_conflict_fix": "Fix: Download Snapchat from the Play Store and uninstall it without keeping data. Run Auto Patcher again. If it still does not work, run:",
"issues_adb_command": "adb uninstall com.snapchat.android",
"issues_invalid_issue": "Issue: App not installed because the package appears to be invalid.",
"issues_invalid_fix": "Fix: Download and install JingMatrix LSPatch, then patch a Snapchat version (any one) from this range, i.e. between 13.65.1.0 and 13.71.0.51, in Integrated mode. Select Embed Modules and embed the PurrfectSnap APK. Then choose Skip auto setup during PurrfectSnap setup to skip Auto Patcher.",
"status_downloading": "Downloading Snapchat {percent}%",
"status_patching": "Patching...",
"status_initializing": "Initializing...",
"logs_copied": "Logs copied to clipboard.",
"install_success": "Patched APK installed",
"start_button": "Start auto patch",
"install_button": "Install patched Snapchat",
"already_installed_button": "Already Installed?",
"powered_by_label": "Powered by Jingmatrix Lspatch",
"logs_title": "Logs",
"copy_button": "Copy",
"log_line_prefix": "- {line}"
}, },
"permissions": { "permissions": {
"dialog": "Complete these essentials to continue:", "dialog": "Complete these essentials to continue:",
"notification_access": "Notification Access", "notification_access": "Notification Access",
"battery_optimization": "Battery Optimization", "battery_optimization": "Battery Optimization",
"display_over_other_apps": "Display Over Other Apps", "display_over_other_apps": "Display Over Other Apps",
"request_button": "Request" "request_button": "Request",
"notification_access_description": "Alerts you the second downloads finish.",
"battery_optimization_description": "Keeps background tasks alive without being killed.",
"display_over_other_apps_description": "Enables floating overlays while you are in Snapchat.",
"granted_label": "Granted"
},
"pick_language": {
"current_selection": "Current selection",
"browse_languages": "Browse languages",
"change_anytime_hint": "You can change this anytime from PurrfectSnap settings.",
"available_languages": "Available Languages"
},
"root_install": {
"title": "Snapchat Installer",
"ready_log": "Snapchat installer is ready.",
"install_confirmed_log": "Snapchat install confirmed. You're cleared to continue.",
"download_recommended_status": "Downloading recommended Snapchat version ({version})...",
"mark_installed_log": "Marked as installed manually. You're cleared to continue.",
"start_download_log": "Starting Snapchat download for rooted install.",
"uninstall_prompt_status": "Snapchat is installed. Please uninstall it first (don't keep data), then try again.",
"uninstall_prompt_error": "Snapchat still installed. Uninstall it first, to continue.",
"fetching_apk_status": "Fetching recommended Snapchat APK...",
"download_failed_error": "Download failed",
"download_completed_status": "Download completed: {fileName}",
"launching_installer_status": "Launching installer...",
"failed_status": "Failed: {message}",
"status_downloading": "Downloading Snapchat {percent}%",
"status_preparing": "Preparing installer...",
"logs_copied": "Logs copied to clipboard.",
"install_success": "Snapchat installed",
"download_button": "Download Snapchat",
"install_button": "Install Snapchat",
"already_installed_button": "Already Installed?",
"logs_title": "Logs",
"copy_button": "Copy",
"log_line_prefix": "- {line}"
},
"save_folder": {
"description": "Please choose the location where media should be downloaded to.",
"destination_label": "Destination",
"system_default_label": "System default",
"use_default_location_button": "Use default location",
"no_picker_title": "Folder picker unavailable",
"no_picker_message": "Some cloned/dual-app environments block the system folder picker. You can continue using the system default save location, or open the app outside clone mode to select a custom folder.",
"use_default_button": "Use default",
"permission_hint": "PurrfectSnap requires Storage permissions to download and Save Media from Snapchat."
} }
}, },
"scopes": { "scopes": {
@@ -61,10 +200,44 @@
"update_title": "PurrfectSnap Update", "update_title": "PurrfectSnap Update",
"update_content": "Version {version} is available!", "update_content": "Version {version} is available!",
"update_button": "Download", "update_button": "Download",
"hero_tagline": "An Xposed Module meant to enhance your Snapchat experience",
"hero_version_label": "Version: {version} - {channel}",
"hero_build_label": "Build: {build}",
"update_ready_label": "Ready to install",
"purr_aura_active_label": "PurrAura Active!",
"purr_aura_inactive_label": "PurrAura Inactive",
"open_settings_button": "Open Settings",
"wiki_button": "Wiki",
"github_button": "GitHub",
"telegram_button": "Telegram",
"channel_label_stable": "Stable",
"channel_label_prerelease": "Pre-release",
"announcements_button_description": "Announcements",
"update_arch_not_supported_toast": "Your device architecture is not supported for automatic updates.",
"update_download_started_toast": "Download started",
"update_download_completed_toast": "Download completed",
"update_install_failed_toast": "Failed to install update. Check logs for more details.",
"update_download_failed_toast": "Download failed: {error}",
"debug_build_summary_title": "You are running a debug build of PurrfectSnap", "debug_build_summary_title": "You are running a debug build of PurrfectSnap",
"debug_build_summary_content": "Version {versionName} ({versionCode})", "debug_build_summary_content": "Version {versionName} ({versionCode})",
"debug_build_summary_date": "Build date: {date} ({days} days ago)", "debug_build_summary_date": "Build date: {date} ({days} days ago)",
"quick_actions_title": "Quick Actions" "quick_actions_title": "Quick Actions",
"quick_actions_empty_title": "No quick tiles yet",
"quick_actions_empty_subtitle": "Design your dream grid with the actions you use the most.",
"quick_actions_add_tile_button": "Add tile",
"quick_actions_manage_button": "Manage",
"quick_actions_count_label": "{count} curated shortcuts"
},
"home_about": {
"about_title": "PurrfectSnap",
"about_tagline": "An Xposed Module meant to enhance your Snapchat experience!",
"about_lead_developers_title": "Lead Developers",
"about_story_title": "Our Story",
"about_story": "PurrfectSnap was founded on 2nd of October, 2025, as a fork of SnapEnhance by IzTIzRNAL with a vision to provide users the quality Snapchat experience they deserve. This app was just meant to be a minor update in the SnapEnhance repository, but it soon became a separate app wherein the contributors kept adding features. Then the developer <RSR/> joined the team, and this app soon became a huge success. We received much love and support and gained 1K+ downloads in just two days! We thank all users and contributors; without your support, we wouldn't have reached this place. We would also like to convey our huge thanks to rhunk, the lead developer of SnapEnhance, as without him, this app wouldn't even exist. We are immensely grateful to him. Lastly, we would like to thank all our admins, notably: CLASSIC GENIUS, Harry, SUJI>L, Zain & scrodingerspet, who were right there with us from the very beginning. We would also like to thank all testers, notably Leo & Toxic, who tested and reported bugs continuously. We are immensely grateful for your contribution.",
"about_thanks_title": "With love, PurrfectSnap Team",
"about_magic_toast": "Tap 5 times in this screen to see some magic 😉!",
"github_button": "GitHub",
"telegram_button": "Telegram"
}, },
"home_logs": { "home_logs": {
"no_logs_hint": "No logs available", "no_logs_hint": "No logs available",
@@ -72,7 +245,8 @@
"export_logs_button": "Export Logs", "export_logs_button": "Export Logs",
"saving_logs_toast": "Saving logs, this may take a while ...", "saving_logs_toast": "Saving logs, this may take a while ...",
"saved_logs_success_toast": "Logs saved successfully", "saved_logs_success_toast": "Logs saved successfully",
"saved_logs_failure_toast": "Failed to save logs" "saved_logs_failure_toast": "Failed to save logs",
"read_logs_failed_toast": "Failed to read logs!"
}, },
"home_settings": { "home_settings": {
"actions_title": "Actions", "actions_title": "Actions",
@@ -119,11 +293,17 @@
"test_mode_label": "Enable PurrAura", "test_mode_label": "Enable PurrAura",
"disable_feature_loading_label": "Disable Feature Loading", "disable_feature_loading_label": "Disable Feature Loading",
"disable_auto_mapper_label": "Disable Auto Mapper", "disable_auto_mapper_label": "Disable Auto Mapper",
"disable_bypass_indicator_label": "Disable Bypass Indicator" "disable_bypass_indicator_label": "Disable Bypass Indicator",
"open_file_failed_toast": "Failed to open file! {message}",
"import_failed_toast": "Import failed: {message}"
}, },
"tasks": { "tasks": {
"no_tasks": "No tasks", "no_tasks": "No tasks",
"merge_button": "Merge", "merge_button": "Merge",
"summary_active": "{active} active \u00b7 {recent} recent",
"summary_idle": "Idle \u00b7 {recent} recent",
"running_count": "{count} running",
"clear_button_description": "Clear tasks",
"failed_to_open_file": "Failed to open file", "failed_to_open_file": "Failed to open file",
"merge_files_toast": "Merging {count} files", "merge_files_toast": "Merging {count} files",
"remove_selected_tasks_title": "Are you sure you want to remove selected tasks?", "remove_selected_tasks_title": "Are you sure you want to remove selected tasks?",
@@ -167,6 +347,9 @@
"social": { "social": {
"friends_tab": "Friends", "friends_tab": "Friends",
"groups_tab": "Groups", "groups_tab": "Groups",
"search_button_description": "Search",
"close_search_button_description": "Close search",
"clear_search_button_description": "Clear search",
"empty_hint": "Your list is empty for now", "empty_hint": "Your list is empty for now",
"friends_empty_title": "No friends added yet", "friends_empty_title": "No friends added yet",
"groups_empty_title": "No groups synced yet", "groups_empty_title": "No groups synced yet",
@@ -317,6 +500,7 @@
"import_button": "Import", "import_button": "Import",
"import_from_url_button": "Import from URL", "import_from_url_button": "Import from URL",
"import_script_from_url_title": "Import Script from URL", "import_script_from_url_title": "Import Script from URL",
"import_failed": "Import failed: {message}",
"import_script_warning": "Only install scripts from sources you trust.", "import_script_warning": "Only install scripts from sources you trust.",
"installed_scripts_tab": "Installed", "installed_scripts_tab": "Installed",
"manage_repos_button": "Manage repos", "manage_repos_button": "Manage repos",
@@ -420,6 +604,7 @@
"delete_rule_dialog_text": "Are you sure you want to delete this rule?", "delete_rule_dialog_text": "Are you sure you want to delete this rule?",
"no_repos_added": "No repositories added", "no_repos_added": "No repositories added",
"import_dialog_title": "Import Rules", "import_dialog_title": "Import Rules",
"read_file_failed_toast": "Failed to read file: {message}",
"bulk_import_button": "Bulk import", "bulk_import_button": "Bulk import",
"individual_import_button": "Single import", "individual_import_button": "Single import",
"invalid_import_type_dialog_title": "Invalid import", "invalid_import_type_dialog_title": "Invalid import",
@@ -753,6 +938,10 @@
"name": "Allow Duplicate", "name": "Allow Duplicate",
"description": "Allows the same media to be downloaded multiple times" "description": "Allows the same media to be downloaded multiple times"
}, },
"file_hash_check": {
"name": "File Hash Check",
"description": "Verify downloaded media with file hashes"
},
"merge_overlays": { "merge_overlays": {
"name": "Merge Overlays", "name": "Merge Overlays",
"description": "Combines the Text and the media of a Snap into a single file" "description": "Combines the Text and the media of a Snap into a single file"
@@ -2834,6 +3023,8 @@
"native_hooks_send_failure_toast": "Failed to send! Please enable Native Hooks in the settings.", "native_hooks_send_failure_toast": "Failed to send! Please enable Native Hooks in the settings.",
"no_participants_to_encrypt_toast": "You don't have any friends in this conversation to encrypt messages with!", "no_participants_to_encrypt_toast": "You don't have any friends in this conversation to encrypt messages with!",
"encryption_failed_toast": "Failed to encrypt message! Check logcat for more details.", "encryption_failed_toast": "Failed to encrypt message! Check logcat for more details.",
"missing_friend_id_toast": "Can't find friendId for conversationId {conversationId}",
"key_exchange_failed_toast": "Can't create key exchange for friendId {friendId}",
"accept_public_key_success_toast": "Public key successfully accepted!", "accept_public_key_success_toast": "Public key successfully accepted!",
"accept_secret_key_success_toast": "Done! You can now send and receive encrypted messages with this friend.", "accept_secret_key_success_toast": "Done! You can now send and receive encrypted messages with this friend.",
"accept_public_key_failure_toast": "Failed to accept public key", "accept_public_key_failure_toast": "Failed to accept public key",
@@ -2845,6 +3036,21 @@
"incoming_pk_message": "You just received a public key request. Click below to accept it.", "incoming_pk_message": "You just received a public key request. Click below to accept it.",
"incoming_secret_message": "Your friend just accepted your public key. Click below to accept the secret." "incoming_secret_message": "Your friend just accepted your public key. Click below to accept the secret."
}, },
"account_switcher_ui": {
"already_logged_in": "Already logged in as {username}",
"login_failed_toast": "Failed to login. Check logs for more info.",
"logged_out_toast": "Logged out",
"data_not_found_toast": "Account data not found",
"restore_failed_toast": "Failed to restore account data",
"logged_in_as_toast": "Logged in as {username}",
"backup_success_toast": "Account backed up!",
"backup_failure_toast": "Failed to backup account. Check logs for more info.",
"import_success_toast": "Imported {username}!",
"import_failure_toast": "Failed to import account: {message}",
"export_success_toast": "Account exported!",
"export_failed_toast": "Failed to export account. Check logs for more info.",
"forced_logout_toast": "Removed account due to forced logout"
},
"auto_open_snaps": { "auto_open_snaps": {
"title": "Auto Open Snaps", "title": "Auto Open Snaps",
"priority_title": "Auto Open Snaps (Priority)", "priority_title": "Auto Open Snaps (Priority)",
@@ -3004,7 +3210,8 @@
"auto_delete_sent_messages": { "auto_delete_sent_messages": {
"countdown_toast": "Message will be deleted in {time}", "countdown_toast": "Message will be deleted in {time}",
"delete_success_toast": "Message deleted successfully", "delete_success_toast": "Message deleted successfully",
"delete_failed_toast": "Failed to delete message" "delete_failed_toast": "Failed to delete message",
"queue_cleared_toast": "Auto delete queue cleared"
}, },
"translation_position": { "translation_position": {
"above": "Above", "above": "Above",
@@ -3243,6 +3450,9 @@
"cancel": "Cancel", "cancel": "Cancel",
"documentation": "Documentation" "documentation": "Documentation"
}, },
"button": {
"cancel": "Cancel"
},
"common": { "common": {
"cancel": "Cancel", "cancel": "Cancel",
"add": "Add", "add": "Add",
@@ -3309,6 +3519,32 @@
"failed_to_fetch_message": "Failed to fetch message: {error}", "failed_to_fetch_message": "Failed to fetch message: {error}",
"failed_to_edit_message": "Failed to edit message: {error}" "failed_to_edit_message": "Failed to edit message: {error}"
}, },
"toast_snapchat_not_installed": "Can't execute action: Snapchat is not installed",
"invalid_input_toast": "Invalid input! Make sure you entered a valid value.",
"toast_async_task_failed": "Async task failed: {message}",
"toast_snapchat_crashed": "Snapchat has crashed! Please check logs for more details.",
"toast_init_features_failed": "Failed to initialize features! Some functionality may not work properly.",
"toast_init_script_runtime_failed": "Failed to initialize script runtime!",
"toast_database_corrupted": "Database {path} is corrupted! Restarting ...",
"toast_feature_init_failed": "Failed to init feature {feature}! Check logcat for more details.",
"toast_updating_purrfectsnap": "Updating PurrfectSnap. Please wait...",
"toast_update_purrfectsnap_failed": "Failed to update PurrfectSnap. Please check logcat for more details.",
"toast_purrfectsnap_updated": "PurrfectSnap updated!",
"toast_export_memories_failed": "Failed to export memories",
"toast_exported_to_path": "Exported to {path}",
"toast_open_memories_db_failed": "Failed to open memories database",
"toast_friend_add_unavailable": "Failed to add friend: FriendRelationshipChanger instance not available",
"toast_friend_add_failed": "Failed to add friend: {message}",
"toast_friends_exported": "Exported {count} friends!",
"toast_friends_import_failed": "Failed to import friends: {message}",
"toast_translation_service_unavailable": "Translation service temporarily unavailable",
"toast_send_message_failed": "Failed to send message: {error}",
"toast_mark_conversation_read_failed": "Failed to mark conversation as read",
"toast_fetch_conversation_failed": "Failed to fetch conversation",
"toast_open_snap_failed": "Failed to open snap",
"toast_mark_message_read_failed": "Failed to mark message as read. Check logs for more details",
"toast_open_conversation_first": "You must open a conversation first",
"toast_open_link_failed": "Failed to open link",
"ai_response_style": { "ai_response_style": {
"casual": "Casual", "casual": "Casual",
"formal": "Formal", "formal": "Formal",
@@ -3347,7 +3583,3 @@
"openrouter": "OpenRouter" "openrouter": "OpenRouter"
} }
} }

View File

@@ -33,11 +33,26 @@ class LocaleWrapper(
lateinit var loadedLocale: Locale lateinit var loadedLocale: Locale
private fun load(locale: String, pfd: ParcelFileDescriptor) { private fun load(locale: String, pfd: ParcelFileDescriptor) {
loadedLocale = if (locale.contains("_")) { loadedLocale = when (locale) {
val split = locale.split("_") "zh_SIMPLIFIED" -> Locale.SIMPLIFIED_CHINESE
Locale.Builder().setLanguage(split[0]).setRegion(split[1]).build() else -> {
} else { if (locale.contains("_")) {
Locale.Builder().setLanguage(locale).build() val split = locale.split("_", limit = 2)
val language = split[0]
val region = split.getOrNull(1)
runCatching {
val builder = Locale.Builder().setLanguage(language)
if (!region.isNullOrBlank() && region.length in 2..3) {
builder.setRegion(region)
}
builder.build()
}.getOrElse {
Locale.forLanguageTag(locale.replace('_', '-'))
}
} else {
Locale.forLanguageTag(locale)
}
}
} }
val translations = AutoCloseInputStream(pfd).use { val translations = AutoCloseInputStream(pfd).use {

View File

@@ -42,7 +42,7 @@ fun Context.getUrlFromClipboard(): String? {
return getTextFromClipboard()?.takeIf { it.startsWith("http") } return getTextFromClipboard()?.takeIf { it.startsWith("http") }
} }
fun Context.openLink(url: String, shouldThrow: Boolean = false) { fun Context.openLink(url: String, failureMessage: String, shouldThrow: Boolean = false) {
runCatching { runCatching {
startActivity(Intent(Intent.ACTION_VIEW).apply { startActivity(Intent(Intent.ACTION_VIEW).apply {
data = url.toUri() data = url.toUri()
@@ -50,7 +50,7 @@ fun Context.openLink(url: String, shouldThrow: Boolean = false) {
}) })
}.onFailure { }.onFailure {
if (shouldThrow) throw it if (shouldThrow) throw it
Toast.makeText(this, "Failed to open link", Toast.LENGTH_SHORT).show() Toast.makeText(this, failureMessage, Toast.LENGTH_SHORT).show()
} }
} }

View File

@@ -100,7 +100,7 @@ class ModContext(
runCatching { runCatching {
runnable() runnable()
}.onFailure { }.onFailure {
longToast("Async task failed: " + it.message) longToast(translation.format("toast_async_task_failed", "message" to (it.message ?: "")))
log.error("Async task failed", it) log.error("Async task failed", it)
} }
} }
@@ -139,7 +139,7 @@ class ModContext(
fun logCritical(message: Any?, throwable: Throwable = Throwable()) { fun logCritical(message: Any?, throwable: Throwable = Throwable()) {
log.error(message ?: "Snapchat crash", throwable) log.error(message ?: "Snapchat crash", throwable)
longToast(message ?: "Snapchat has crashed! Please check logs for more details.") longToast(message ?: translation["toast_snapchat_crashed"])
} }
private fun delayForceCloseApp(delay: Long) = Handler(Looper.getMainLooper()).postDelayed({ private fun delayForceCloseApp(delay: Long) = Handler(Looper.getMainLooper()).postDelayed({

View File

@@ -198,7 +198,7 @@ class PurrfectSnap {
log.verbose("Features initialized successfully") log.verbose("Features initialized successfully")
}.onFailure { throwable -> }.onFailure { throwable ->
log.error("Failed to initialize features", throwable) log.error("Failed to initialize features", throwable)
longToast("Failed to initialize features! Some functionality may not work properly.") longToast(appContext.translation["toast_init_features_failed"])
// Continue with other initializations even if features fail // Continue with other initializations even if features fail
} }
@@ -209,7 +209,7 @@ class PurrfectSnap {
log.verbose("Script runtime initialized successfully") log.verbose("Script runtime initialized successfully")
}.onFailure { throwable -> }.onFailure { throwable ->
log.error("Failed to initialize script runtime", throwable) log.error("Failed to initialize script runtime", throwable)
longToast("Failed to initialize script runtime!") longToast(appContext.translation["toast_init_script_runtime_failed"])
} }
} }
} }

View File

@@ -299,13 +299,15 @@ class ExportMemories : AbstractAction() {
val exportedPath = runCatching { outputTarget.finalize(outputZip) } val exportedPath = runCatching { outputTarget.finalize(outputZip) }
.getOrElse { error -> .getOrElse { error ->
context.log.error("Failed to finalize memories export", error) context.log.error("Failed to finalize memories export", error)
context.longToast("Failed to export memories") context.longToast(context.translation["toast_export_memories_failed"])
return return
} }
if (outputZip.parentFile == context.androidContext.cacheDir) { if (outputZip.parentFile == context.androidContext.cacheDir) {
outputZip.delete() outputZip.delete()
} }
context.longToast("Exported to $exportedPath") context.longToast(
context.translation.format("toast_exported_to_path", "path" to exportedPath)
)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -863,7 +865,7 @@ class ExportMemories : AbstractAction() {
}.getOrNull() }.getOrNull()
if (database == null) { if (database == null) {
context.longToast("Failed to open memories database") context.longToast(context.translation["toast_open_memories_db_failed"])
return@launch return@launch
} }

View File

@@ -87,7 +87,7 @@ class ManageFriendList : AbstractAction() {
private fun addFriend(userId: String) { private fun addFriend(userId: String) {
val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance
?: run { ?: run {
context.longToast("Failed to add friend: FriendRelationshipChanger instance not available") context.longToast(context.translation["toast_friend_add_unavailable"])
return return
} }
@@ -153,7 +153,9 @@ class ManageFriendList : AbstractAction() {
} }
}.onFailure { }.onFailure {
context.log.error("Failed to add friend $userId", it) context.log.error("Failed to add friend $userId", it)
context.longToast("Failed to add friend: ${it.message}") context.longToast(
context.translation.format("toast_friend_add_failed", "message" to (it.message ?: ""))
)
} }
} }
} }
@@ -173,7 +175,9 @@ class ManageFriendList : AbstractAction() {
context.androidContext.contentResolver.openOutputStream(data)?.bufferedWriter()?.use { writer -> context.androidContext.contentResolver.openOutputStream(data)?.bufferedWriter()?.use { writer ->
userIds.forEach { writer.write(it); writer.newLine() } userIds.forEach { writer.write(it); writer.newLine() }
} }
context.longToast("Exported ${userIds.size} friends!") context.longToast(
context.translation.format("toast_friends_exported", "count" to userIds.size.toString())
)
} }
context.mainActivity?.startActivityForResult( context.mainActivity?.startActivityForResult(
Intent.createChooser(Intent(Intent.ACTION_CREATE_DOCUMENT).apply { Intent.createChooser(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
@@ -303,7 +307,12 @@ class ManageFriendList : AbstractAction() {
fetchedFriends = context.androidContext.contentResolver.openInputStream(data)?.bufferedReader()?.readLines()?.filter { it.matches(uuidRegex) }?.map { it.trim() }?.toMutableList() ?: mutableListOf() fetchedFriends = context.androidContext.contentResolver.openInputStream(data)?.bufferedReader()?.readLines()?.filter { it.matches(uuidRegex) }?.map { it.trim() }?.toMutableList() ?: mutableListOf()
}.onFailure { }.onFailure {
context.log.error("Failed to import friends", it) context.log.error("Failed to import friends", it)
context.longToast("Failed to import friends: ${it.message}") context.longToast(
context.translation.format(
"toast_friends_import_failed",
"message" to (it.message ?: "")
)
)
} }
} }
context.mainActivity?.startActivityForResult(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply { type = "*/*" }, "Select a file"), pendingPickerAction!!.first) context.mainActivity?.startActivityForResult(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply { type = "*/*" }, "Select a file"), pendingPickerAction!!.first)

View File

@@ -99,7 +99,12 @@ class DatabaseAccess(
context.log.error("Failed to execute query $query", it) context.log.error("Failed to execute query $query", it)
return@onFailure return@onFailure
} }
context.longToast("Database ${this.path} is corrupted! Restarting ...") context.longToast(
context.translation.format(
"toast_database_corrupted",
"path" to this.path
)
)
context.androidContext.deleteDatabase(this.path) context.androidContext.deleteDatabase(this.path)
context.crash("Database ${this.path} is corrupted!", it) context.crash("Database ${this.path} is corrupted!", it)
}.getOrNull() }.getOrNull()
@@ -616,4 +621,4 @@ class DatabaseAccess(
} }
} }
} }
} }

View File

@@ -164,7 +164,12 @@ class FeatureManager(
} }
}.onFailure { }.onFailure {
context.log.error("Failed to init feature ${feature.key}", it) context.log.error("Failed to init feature ${feature.key}", it)
context.longToast("Failed to init feature ${feature.key}! Check logcat for more details.") context.longToast(
context.translation.format(
"toast_feature_init_failed",
"feature" to feature.key
)
)
} }
} }
} }

View File

@@ -48,6 +48,7 @@ import java.util.zip.ZipOutputStream
import kotlin.random.Random import kotlin.random.Random
class AccountSwitcher: Feature("Account Switcher") { class AccountSwitcher: Feature("Account Switcher") {
private val translation by lazy { context.translation.getCategory("account_switcher_ui") }
private var exportCallback: Pair<Int, String>? = null // requestCode -> userId private var exportCallback: Pair<Int, String>? = null // requestCode -> userId
private var importRequestCode: Int? = null private var importRequestCode: Int? = null
@@ -107,7 +108,9 @@ class AccountSwitcher: Feature("Account Switcher") {
onClick = { onClick = {
runCatching { runCatching {
if (!isLoginActivity && context.database.myUserId == user.first) { if (!isLoginActivity && context.database.myUserId == user.first) {
context.shortToast("Already logged in as ${user.second}") context.shortToast(
translation.format("already_logged_in", "username" to user.second)
)
return@runCatching return@runCatching
} }
@@ -117,7 +120,7 @@ class AccountSwitcher: Feature("Account Switcher") {
login(userId = user.first, username = user.second) login(userId = user.first, username = user.second)
}.onFailure { }.onFailure {
context.shortToast("Failed to login. Check logs for more info.") context.shortToast(translation["login_failed_toast"])
context.log.error("Failed to login", it) context.log.error("Failed to login", it)
} }
} }
@@ -259,7 +262,7 @@ class AccountSwitcher: Feature("Account Switcher") {
private fun logout() { private fun logout() {
context.androidContext.dataDir.resolve( "shared_prefs/user_session_shared_pref.xml").takeIf { it.exists() }?.delete() context.androidContext.dataDir.resolve( "shared_prefs/user_session_shared_pref.xml").takeIf { it.exists() }?.delete()
context.shortToast("Logged out") context.shortToast(translation["logged_out_toast"])
context.softRestartApp() context.softRestartApp()
} }
@@ -268,7 +271,7 @@ class AccountSwitcher: Feature("Account Switcher") {
ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.readBytes() } ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.readBytes() }
} }
if (accountData == null) { if (accountData == null) {
context.shortToast("Account data not found") context.shortToast(translation["data_not_found_toast"])
return return
} }
@@ -312,12 +315,12 @@ class AccountSwitcher: Feature("Account Switcher") {
zipInputStream.close() zipInputStream.close()
} catch (e: Exception) { } catch (e: Exception) {
context.log.error("Failed to restore account data", e) context.log.error("Failed to restore account data", e)
context.shortToast("Failed to restore account data") context.shortToast(translation["restore_failed_toast"])
return return
} }
context.log.debug("Account data restored") context.log.debug("Account data restored")
context.shortToast("Logged in as $username") context.shortToast(translation.format("logged_in_as_toast", "username" to username))
context.softRestartApp() context.softRestartApp()
} }
@@ -390,9 +393,9 @@ class AccountSwitcher: Feature("Account Switcher") {
context.database.getFriendInfo(context.database.myUserId)?.mutableUsername ?: "Unknown username", context.database.getFriendInfo(context.database.myUserId)?.mutableUsername ?: "Unknown username",
getCurrentAccountData() getCurrentAccountData()
) )
context.shortToast("Account backed up!") context.shortToast(translation["backup_success_toast"])
}.onFailure { }.onFailure {
context.shortToast("Failed to backup account. Check logs for more info.") context.shortToast(translation["backup_failure_toast"])
context.log.error("Failed to backup account", it) context.log.error("Failed to backup account", it)
} }
} }
@@ -465,11 +468,13 @@ class AccountSwitcher: Feature("Account Switcher") {
it.toParcelFileDescriptor(context.coroutineScope) it.toParcelFileDescriptor(context.coroutineScope)
) )
} }
context.shortToast("Imported $username!") context.shortToast(translation.format("import_success_toast", "username" to username))
updateUsers() updateUsers()
} }
}.onFailure { }.onFailure {
context.shortToast("Failed to import account: ${it.message}") context.shortToast(
translation.format("import_failure_toast", "message" to (it.message ?: ""))
)
context.log.error("Failed to import account", it) context.log.error("Failed to import account", it)
} }
@@ -522,10 +527,10 @@ class AccountSwitcher: Feature("Account Switcher") {
it.copyTo(outputStream) it.copyTo(outputStream)
} }
} }
context.shortToast("Account exported!") context.shortToast(translation["export_success_toast"])
} }
}.onFailure { }.onFailure {
context.shortToast("Failed to export account. Check logs for more info.") context.shortToast(translation["export_failed_toast"])
context.log.error("Failed to export account", it) context.log.error("Failed to export account", it)
} }
} }
@@ -539,10 +544,10 @@ class AccountSwitcher: Feature("Account Switcher") {
runCatching { runCatching {
val accountStorage = context.bridgeClient.getAccountStorage() val accountStorage = context.bridgeClient.getAccountStorage()
if (accountStorage.isAccountExists(context.database.myUserId)) { if (accountStorage.isAccountExists(context.database.myUserId)) {
accountStorage.removeAccount(context.database.myUserId) accountStorage.removeAccount(context.database.myUserId)
context.shortToast("Removed account due to forced logout") context.shortToast(translation["forced_logout_toast"])
} }
} }
return@hook return@hook
} }

View File

@@ -75,12 +75,22 @@ class EndToEndEncryption : MessagingRuleFeature(
private fun askForKeys(conversationId: String) { private fun askForKeys(conversationId: String) {
val friendId = context.database.getDMOtherParticipant(conversationId) ?: run { val friendId = context.database.getDMOtherParticipant(conversationId) ?: run {
context.longToast("Can't find friendId for conversationId $conversationId") context.longToast(
translation.format(
"missing_friend_id_toast",
"conversationId" to conversationId
)
)
return return
} }
val publicKey = e2eeInterface.createKeyExchange(friendId) ?: run { val publicKey = e2eeInterface.createKeyExchange(friendId) ?: run {
context.longToast("Can't create key exchange for friendId $friendId") context.longToast(
translation.format(
"key_exchange_failed_toast",
"friendId" to friendId
)
)
return return
} }
@@ -131,7 +141,12 @@ class EndToEndEncryption : MessagingRuleFeature(
private fun handlePublicKeyRequest(conversationId: String, publicKey: ByteArray) { private fun handlePublicKeyRequest(conversationId: String, publicKey: ByteArray) {
val friendId = context.database.getDMOtherParticipant(conversationId) ?: run { val friendId = context.database.getDMOtherParticipant(conversationId) ?: run {
context.longToast("Can't find friendId for conversationId $conversationId") context.longToast(
translation.format(
"missing_friend_id_toast",
"conversationId" to conversationId
)
)
return return
} }
warnKeyOverwrite(friendId) { warnKeyOverwrite(friendId) {
@@ -151,7 +166,12 @@ class EndToEndEncryption : MessagingRuleFeature(
private fun handleSecretResponse(conversationId: String, secret: ByteArray) { private fun handleSecretResponse(conversationId: String, secret: ByteArray) {
val friendId = context.database.getDMOtherParticipant(conversationId) ?: run { val friendId = context.database.getDMOtherParticipant(conversationId) ?: run {
context.longToast("Can't find friendId for conversationId $conversationId") context.longToast(
translation.format(
"missing_friend_id_toast",
"conversationId" to conversationId
)
)
return return
} }
warnKeyOverwrite(friendId) { warnKeyOverwrite(friendId) {
@@ -561,4 +581,4 @@ class EndToEndEncryption : MessagingRuleFeature(
} }
override fun getRuleState() = RuleState.WHITELIST override fun getRuleState() = RuleState.WHITELIST
} }

View File

@@ -396,7 +396,7 @@ class AutoDeleteSentMessages : MessagingRuleFeature("Auto Delete Sent Messages",
notificationManager.cancel(9999) notificationManager.cancel(9999)
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
context.shortToast("Auto delete queue cleared") context.shortToast(translation["queue_cleared_toast"])
} }
} }
} }

View File

@@ -91,7 +91,7 @@ class MessageTranslator : Feature("Instant Translation") {
if (config.pauseOnError.get()) { if (config.pauseOnError.get()) {
isPaused = true isPaused = true
context.log.warn("Translation paused due to errors") context.log.warn("Translation paused due to errors")
context.shortToast("Translation service temporarily unavailable") context.shortToast(context.translation["toast_translation_service_unavailable"])
} }
} }
} }

View File

@@ -192,7 +192,12 @@ class Notifications : Feature("Notifications") {
val myUser = context.database.myUserId.let { context.database.getFriendInfo(it) } ?: return@subscribe val myUser = context.database.myUserId.let { context.database.getFriendInfo(it) } ?: return@subscribe
context.messageSender.sendChatMessage(listOf(SnapUUID(conversationId)), input, onError = { context.messageSender.sendChatMessage(listOf(SnapUUID(conversationId)), input, onError = {
context.longToast("Failed to send message: $it") context.longToast(
context.translation.format(
"toast_send_message_failed",
"error" to it.toString()
)
)
context.coroutineScope.launch(coroutineDispatcher) { context.coroutineScope.launch(coroutineDispatcher) {
appendNotificationText("Failed to send message: $it") appendNotificationText("Failed to send message: $it")
} }
@@ -221,7 +226,7 @@ class Notifications : Feature("Notifications") {
onResult = { onResult = {
if (it != null) { if (it != null) {
context.log.error("Failed to mark conversation as read: $it") context.log.error("Failed to mark conversation as read: $it")
context.shortToast("Failed to mark conversation as read") context.shortToast(context.translation["toast_mark_conversation_read_failed"])
} }
} }
) )
@@ -245,7 +250,7 @@ class Notifications : Feature("Notifications") {
}, },
onError = { onError = {
context.log.error("Failed to fetch conversation: $it") context.log.error("Failed to fetch conversation: $it")
context.shortToast("Failed to fetch conversation") context.shortToast(context.translation["toast_fetch_conversation_failed"])
} }
) )
} }
@@ -257,13 +262,13 @@ class Notifications : Feature("Notifications") {
conversationManager.updateMessage(conversationId, clientMessageId, MessageUpdate.READ) { conversationManager.updateMessage(conversationId, clientMessageId, MessageUpdate.READ) {
if (it != null) { if (it != null) {
context.log.error("Failed to open snap: $it") context.log.error("Failed to open snap: $it")
context.shortToast("Failed to open snap") context.shortToast(context.translation["toast_open_snap_failed"])
} }
} }
} }
}.onFailure { }.onFailure {
context.log.error("Failed to mark message as read", it) context.log.error("Failed to mark message as read", it)
context.shortToast("Failed to mark message as read. Check logs for more details") context.shortToast(context.translation["toast_mark_message_read_failed"])
} }
notificationManager.cancel(notificationId) notificationManager.cancel(notificationId)
} }

View File

@@ -113,7 +113,7 @@ class ConversationToolbox : Feature("Conversation Toolbox") {
private fun openToolbox() { private fun openToolbox() {
val openedConversationId = context.feature(Messaging::class).openedConversationUUID?.toString() ?: run { val openedConversationId = context.feature(Messaging::class).openedConversationUUID?.toString() ?: run {
context.shortToast("You must open a conversation first") context.shortToast(context.translation["toast_open_conversation_first"])
return return
} }

View File

@@ -59,19 +59,19 @@ object LSPatchUpdater {
} }
context.log.verbose("updating", TAG) context.log.verbose("updating", TAG)
context.shortToast("Updating PurrfectSnap. Please wait...") context.shortToast(context.translation["toast_updating_purrfectsnap"])
// copy embedded module to cache // copy embedded module to cache
runCatching { runCatching {
seAppApk.copyTo(embeddedModule, overwrite = true) seAppApk.copyTo(embeddedModule, overwrite = true)
}.onFailure { }.onFailure {
seAppApk.delete() seAppApk.delete()
context.log.error("Failed to copy embedded module", it, TAG) context.log.error("Failed to copy embedded module", it, TAG)
context.longToast("Failed to update PurrfectSnap. Please check logcat for more details.") context.longToast(context.translation["toast_update_purrfectsnap_failed"])
context.forceCloseApp() context.forceCloseApp()
return return
} }
context.longToast("PurrfectSnap updated!") context.longToast(context.translation["toast_purrfectsnap_updated"])
context.log.verbose("updated", TAG) context.log.verbose("updated", TAG)
context.softRestartApp() context.softRestartApp()
} }