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
)
if (intent == null) {
shortToast("Can't execute action: Snapchat is not installed")
shortToast(translation["toast_snapchat_not_installed"])
return
}
intent.putExtra(EnumAction.ACTION_PARAMETER, action.key)

View File

@@ -91,6 +91,7 @@ object UpdateDownloader {
scope: CoroutineScope
) {
val context = remoteContext.androidContext
val translation = remoteContext.translation.getCategory("manager.sections.home")
val fetch = getInstance(remoteContext)
val filePath = File(context.externalCacheDir, fileName).path
remoteContext.log.info("Starting update download from $downloadUrl -> $filePath", TAG)
@@ -107,7 +108,7 @@ object UpdateDownloader {
override fun onQueued(download: Download, waitingOnNetwork: Boolean) {
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) {
@@ -122,7 +123,7 @@ object UpdateDownloader {
"Download completed -> ${downloadedFile.absolutePath} (${downloadedFile.length()} bytes)",
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 uri = FileProvider.getUriForFile(
context,
@@ -144,7 +145,7 @@ object UpdateDownloader {
remoteContext.log.info("Cleaned downloaded update files", TAG)
}
}.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)
downloadState.value = DownloadState.FAILED
}
@@ -157,7 +158,11 @@ object UpdateDownloader {
override fun onError(download: Download, error: Error, throwable: Throwable?) {
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) }
?: remoteContext.log.error("Update download failed: $error", TAG)
fetch.removeListener(this)

View File

@@ -414,7 +414,7 @@ class TasksRootSection : Routes.Route() {
IconButton(onClick = {
showConfirmDialog = true
}) {
Icon(Icons.Filled.Delete, contentDescription = "Clear tasks")
Icon(Icons.Filled.Delete, contentDescription = translation["clear_button_description"])
}
if (showConfirmDialog) {
@@ -917,7 +917,18 @@ class TasksRootSection : Routes.Route() {
fontSize = 18.sp
)
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,
fontSize = 12.sp
)
@@ -964,11 +975,16 @@ class TasksRootSection : Routes.Route() {
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
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 }) {
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"],
text = translation["dialog_clear_confirmation_text"],
icon = Icons.Default.DeleteSweep,
confirmButtonText = "Clear",
confirmButtonText = context.translation["clear"],
dismissButtonText = context.translation["button.cancel"],
onDismiss = { confirmationDialog = false },
onConfirm = {
@@ -390,7 +390,7 @@ class ManageRuleFeature : Routes.Route() {
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))
}
val scrollState = rememberScrollState()
val aboutStory = remember {
"""
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 aboutStory = remember { translation["about_story"] }
val pagePadding = 16.dp
val bottomPadding = routes.bottomPadding +
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +
@@ -82,7 +76,7 @@ class HomeAbout : Routes.Route() {
val lastTapTime = remember { mutableLongStateOf(0L) }
LaunchedEffect(Unit) {
context.shortToast("Tap 5 times in this screen to see some magic 😉!")
context.shortToast(translation["about_magic_toast"])
}
Box(
@@ -97,7 +91,7 @@ class HomeAbout : Routes.Route() {
.padding(bottom = bottomPadding)
) {
FloatingTopBar(
title = routeInfo.translatedKey?.value ?: "About",
title = routeInfo.translatedKey?.value ?: translation["manager.routes.home_about"],
onBack = { routes.navController.popBackStack() }
)
@@ -121,7 +115,7 @@ class HomeAbout : Routes.Route() {
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "PurrfectSnap",
text = translation["about_title"],
fontSize = 28.sp,
fontWeight = FontWeight.ExtraBold,
color = PurrfectPalette.textPrimary,
@@ -143,14 +137,14 @@ class HomeAbout : Routes.Route() {
}
)
Text(
text = "An Xposed Module meant to enhance your Snapchat experience!",
text = translation["about_tagline"],
fontSize = 13.sp,
color = PurrfectPalette.textSecondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "Lead Developers",
text = translation["about_lead_developers_title"],
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,
@@ -196,7 +190,7 @@ class HomeAbout : Routes.Route() {
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Our Story",
text = translation["about_story_title"],
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White
@@ -228,7 +222,7 @@ class HomeAbout : Routes.Route() {
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "With love, PurrfectSnap Team",
text = translation["about_thanks_title"],
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,
@@ -242,7 +236,12 @@ class HomeAbout : Routes.Route() {
) {
Button(
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(
containerColor = Color.White,
contentColor = Color(0xFF1B152E)
@@ -254,11 +253,16 @@ class HomeAbout : Routes.Route() {
modifier = Modifier.size(18.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(
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)),
colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White)
) {
@@ -269,7 +273,7 @@ class HomeAbout : Routes.Route() {
tint = Color.White
)
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 {
context.longToast("Failed to read logs!")
context.longToast(translation["read_logs_failed_toast"])
}
readerResult.getOrNull()?.let { reader ->
logReader = reader

View File

@@ -319,7 +319,7 @@ class HomeRootSection : Routes.Route() {
) { routes.homeLogs.navigate() }
TopBarActionChip(
icon = Icons.Filled.Info,
label = "About"
label = translation["manager.routes.home_about"]
) { routes.about.navigate() }
}
@@ -453,7 +453,7 @@ class HomeRootSection : Routes.Route() {
fontFamily = avenirNext
)
Text(
text = "An Xposed Module meant to enhance your Snapchat experience",
text = translation["hero_tagline"],
color = Color.White.copy(alpha = 0.9f),
fontSize = 15.sp,
lineHeight = 20.sp,
@@ -465,9 +465,9 @@ class HomeRootSection : Routes.Route() {
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
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 {
HeroBadge("Build: $it")
HeroBadge(translation.format("hero_build_label", "build" to it))
}
}
@@ -566,7 +566,7 @@ class HomeRootSection : Routes.Route() {
tint = Color(0xFFA3F0C2)
)
Text(
text = "Ready to install",
text = translation["update_ready_label"],
color = Color.White,
fontWeight = FontWeight.SemiBold
)
@@ -611,7 +611,7 @@ class HomeRootSection : Routes.Route() {
.background(if (isPurrAuraActive) PurrfectPalette.glowPrimary else Color(0xFF8C8CA3))
)
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,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
@@ -627,7 +627,7 @@ class HomeRootSection : Routes.Route() {
) {
Icon(Icons.Filled.Settings, contentDescription = null, tint = Color.White)
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))
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(
modifier = Modifier.weight(1f),
@@ -672,7 +672,7 @@ class HomeRootSection : Routes.Route() {
modifier = Modifier.size(18.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(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_telegram),
@@ -740,7 +740,7 @@ class HomeRootSection : Routes.Route() {
context.database.getQuickTiles().filter { it.isNotBlank() }
}
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 channel = if (updateChannel == "prerelease") Channel.PRERELEASE else Channel.STABLE
Updater.getLatestRelease(channel)
@@ -785,7 +785,7 @@ class HomeRootSection : Routes.Route() {
if (abiName == null) {
android.widget.Toast.makeText(
context.androidContext,
"Your device architecture is not supported for automatic updates.",
translation["update_arch_not_supported_toast"],
android.widget.Toast.LENGTH_LONG
).show()
} else {
@@ -807,7 +807,10 @@ class HomeRootSection : Routes.Route() {
"No matching update asset for arch=$abiName (available: ${latest.assetDownloads.keys})",
"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(
icon = Icons.Filled.Notifications,
label = null,
contentDescription = "Announcements"
contentDescription = translation["announcements_button_description"]
) {
showAnnouncementsDialog = true
loadAnnouncements()
@@ -949,9 +952,24 @@ class HomeRootSection : Routes.Route() {
onUpdateAction = onUpdateButtonClick,
channelLabel = channelLabel,
isPurrAuraActive = isPurrAuraActive,
onWikiClick = { context.androidContext.openLink("https://github.com/particle-box/PurrfectSnap/wiki") },
onTelegramClick = { context.androidContext.openLink("https://t.me/purrfectsnap_official") },
onGithubClick = { context.androidContext.openLink("https://github.com/particle-box/PurrfectSnap") },
onWikiClick = {
context.androidContext.openLink(
"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",
onManageClick = { routes.settings.navigate() },
avenirNext = avenirNext,
@@ -1001,14 +1019,14 @@ class HomeRootSection : Routes.Route() {
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "No quick tiles yet",
text = translation["quick_actions_empty_title"],
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Design your dream grid with the actions you use the most.",
text = translation["quick_actions_empty_subtitle"],
fontSize = 14.sp,
color = Color.White.copy(alpha = 0.75f),
textAlign = TextAlign.Center
@@ -1027,7 +1045,7 @@ class HomeRootSection : Routes.Route() {
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(text = "Add tile")
Text(text = translation["quick_actions_add_tile_button"])
}
}
} else {
@@ -1048,7 +1066,7 @@ class HomeRootSection : Routes.Route() {
overflow = TextOverflow.Clip
)
Text(
text = "${selectedTiles.size} curated shortcuts",
text = translation.format("quick_actions_count_label", "count" to selectedTiles.size.toString()),
fontSize = 13.sp,
color = Color.White.copy(alpha = 0.75f),
textAlign = TextAlign.Center
@@ -1069,7 +1087,7 @@ class HomeRootSection : Routes.Route() {
modifier = Modifier.size(18.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 {
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,
@@ -728,12 +733,22 @@ class HomeSettings : Routes.Route() {
context.log.info("Imported message logger from $uri", "MessageLogger")
}.onFailure {
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 {
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")

View File

@@ -183,7 +183,8 @@ fun ScriptCatalog(root: ScriptingRootSection) {
Button(
onClick = {
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(

View File

@@ -543,10 +543,24 @@ class ScriptingRootSection : Routes.Route() {
if (scriptingFolder == null) showToast = true else showImportDialog = true
},
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() },
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
)
Spacer(Modifier.height(12.dp))

View File

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

View File

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

View File

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

View File

@@ -98,10 +98,14 @@ class InstallModeScreen(
}
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(
onDismissRequest = { if (timeout == 0) showGuides = false },
title = "Please note!",
title = context.translation["setup.install_mode.notice_title"],
text = "",
icon = Icons.Filled.Warning,
confirmButtonText = confirmLabel,
@@ -138,39 +142,39 @@ class InstallModeScreen(
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
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,
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "Non-rooted devices",
text = context.translation["setup.install_mode.notice_non_root_title"],
fontWeight = FontWeight.SemiBold,
color = Color.White,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Start
)
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,
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "Rooted devices",
text = context.translation["setup.install_mode.notice_root_title"],
fontWeight = FontWeight.SemiBold,
color = Color.White,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Start
)
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,
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth()
)
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,
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth()
@@ -178,9 +182,9 @@ class InstallModeScreen(
Text(
text = buildAnnotatedString {
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,
textAlign = TextAlign.Start,
@@ -194,8 +198,8 @@ class InstallModeScreen(
SetupCard {
StepTitle(
title = "Choose your device",
subtitle = "If you don't know, select Non-rooted device and proceed.",
title = context.translation["setup.install_mode.step_title"],
subtitle = context.translation["setup.install_mode.step_subtitle"],
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Column(
@@ -203,8 +207,8 @@ class InstallModeScreen(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
ModeOption(
title = "Rooted device",
subtitle = "Use Lsposed and skip auto patching.",
title = context.translation["setup.install_mode.root_option_title"],
subtitle = context.translation["setup.install_mode.root_option_subtitle"],
icon = Icons.Filled.VerifiedUser,
accent = Brush.horizontalGradient(
listOf(
@@ -219,8 +223,8 @@ class InstallModeScreen(
}
)
ModeOption(
title = "Non-rooted device",
subtitle = "Use included auto patcher to install patched Snapchat.",
title = context.translation["setup.install_mode.non_root_option_title"],
subtitle = context.translation["setup.install_mode.non_root_option_subtitle"],
icon = Icons.Filled.Shield,
accent = Brush.horizontalGradient(
listOf(
@@ -262,7 +266,7 @@ class InstallModeScreen(
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Skip Auto Setup",
text = context.translation["setup.install_mode.skip_auto_setup"],
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,

View File

@@ -52,6 +52,7 @@ class MappingsScreen : SetupScreen() {
@Composable
override fun Content() {
val coroutineScope = rememberCoroutineScope()
val translation = context.translation
var infoText by remember { mutableStateOf(null as String?) }
var isGenerating by remember { mutableStateOf(false) }
var showCompletionNotice by remember { mutableStateOf(false) }
@@ -98,13 +99,16 @@ class MappingsScreen : SetupScreen() {
if (showCompletionNotice) {
val confirmLabel = if (completionCountdown > 0) {
"I understand (${completionCountdown}s)"
translation.format(
"setup.mappings.confirm_understand_timeout",
"seconds" to completionCountdown.toString()
)
} else {
"I understand"
translation["setup.mappings.confirm_understand"]
}
AestheticDialog(
onDismissRequest = { if (completionCountdown == 0) { showCompletionNotice = false; goNext() } },
title = "Please note!",
title = translation["setup.mappings.notice_title"],
text = "",
icon = Icons.Filled.Warning,
confirmButtonText = confirmLabel,
@@ -141,27 +145,27 @@ class MappingsScreen : SetupScreen() {
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
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,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "1. Reopen Snapchat and log in. This fixes it most of the time.",
text = translation["setup.mappings.notice_step_1"],
style = bodyStyle,
modifier = Modifier.fillMaxWidth()
)
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,
modifier = Modifier.fillMaxWidth()
)
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,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "For rooted users:",
text = translation["setup.mappings.notice_rooted_title"],
style = MaterialTheme.typography.bodyMedium.copy(
color = Color.White,
fontWeight = FontWeight.SemiBold,
@@ -170,7 +174,7 @@ class MappingsScreen : SetupScreen() {
modifier = Modifier.fillMaxWidth()
)
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,
modifier = Modifier.fillMaxWidth()
)
@@ -193,7 +197,11 @@ class MappingsScreen : SetupScreen() {
if (warnings.isNotEmpty()) {
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)
}
return@launch
@@ -216,7 +224,7 @@ class MappingsScreen : SetupScreen() {
subtitle = null
)
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))
Surface(

View File

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

View File

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

View File

@@ -151,7 +151,7 @@ class PickLanguageScreen : SetupScreen() {
modifier = Modifier.weight(1f)
) {
Text(
text = "Current selection",
text = context.translation["setup.pick_language.current_selection"],
fontSize = 14.sp,
color = PurrfectPalette.textSecondary
)
@@ -180,9 +180,9 @@ class PickLanguageScreen : SetupScreen() {
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) {
@@ -196,7 +196,7 @@ class PickLanguageScreen : SetupScreen() {
modifier = Modifier.fillMaxWidth()
) {
StepTitle(
title = "Available Languages",
title = context.translation["setup.pick_language.available_languages"],
subtitle = null,
modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center

View File

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

View File

@@ -58,7 +58,7 @@ class SaveFolderScreen : SetupScreen() {
title = context.translation["setup.dialogs.save_folder"],
subtitle = null
)
DialogText(text = "Please choose the location where media should be downloaded to.")
DialogText(text = context.translation["setup.save_folder.description"])
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(22.dp),
@@ -99,12 +99,12 @@ class SaveFolderScreen : SetupScreen() {
modifier = Modifier.weight(1f)
) {
Text(
text = "Destination",
text = context.translation["setup.save_folder.destination_label"],
fontSize = 13.sp,
color = PurrfectPalette.textSecondary
)
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,
fontWeight = FontWeight.SemiBold,
color = Color.White,
@@ -162,7 +162,7 @@ class SaveFolderScreen : SetupScreen() {
),
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) {
@@ -204,13 +204,13 @@ class SaveFolderScreen : SetupScreen() {
}
}
Text(
text = "Folder picker unavailable",
text = context.translation["setup.save_folder.no_picker_title"],
fontSize = 18.sp,
fontWeight = FontWeight.ExtraBold,
color = Color.White
)
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,
lineHeight = 18.sp,
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))
) {
Text("Cancel")
Text(context.translation["button.cancel"])
}
Button(
onClick = {
@@ -248,7 +248,7 @@ class SaveFolderScreen : SetupScreen() {
contentColor = Color.White
)
) {
Text("Use default")
Text(context.translation["setup.save_folder.use_default_button"])
}
}
}
@@ -256,7 +256,7 @@ class SaveFolderScreen : SetupScreen() {
}
}
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(
onClick = {
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
}

View File

@@ -1,21 +1,160 @@
{
"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": {
"select_language": "Select Language",
"save_folder": "Choose where to save downloads",
"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": {
"dialog": "Generating Mappings...",
"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": {
"dialog": "Complete these essentials to continue:",
"notification_access": "Notification Access",
"battery_optimization": "Battery Optimization",
"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": {
@@ -61,10 +200,44 @@
"update_title": "PurrfectSnap Update",
"update_content": "Version {version} is available!",
"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_content": "Version {versionName} ({versionCode})",
"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": {
"no_logs_hint": "No logs available",
@@ -72,7 +245,8 @@
"export_logs_button": "Export Logs",
"saving_logs_toast": "Saving logs, this may take a while ...",
"saved_logs_success_toast": "Logs saved successfully",
"saved_logs_failure_toast": "Failed to save logs"
"saved_logs_failure_toast": "Failed to save logs",
"read_logs_failed_toast": "Failed to read logs!"
},
"home_settings": {
"actions_title": "Actions",
@@ -119,11 +293,17 @@
"test_mode_label": "Enable PurrAura",
"disable_feature_loading_label": "Disable Feature Loading",
"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": {
"no_tasks": "No tasks",
"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",
"merge_files_toast": "Merging {count} files",
"remove_selected_tasks_title": "Are you sure you want to remove selected tasks?",
@@ -167,6 +347,9 @@
"social": {
"friends_tab": "Friends",
"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",
"friends_empty_title": "No friends added yet",
"groups_empty_title": "No groups synced yet",
@@ -317,6 +500,7 @@
"import_button": "Import",
"import_from_url_button": "Import 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.",
"installed_scripts_tab": "Installed",
"manage_repos_button": "Manage repos",
@@ -420,6 +604,7 @@
"delete_rule_dialog_text": "Are you sure you want to delete this rule?",
"no_repos_added": "No repositories added",
"import_dialog_title": "Import Rules",
"read_file_failed_toast": "Failed to read file: {message}",
"bulk_import_button": "Bulk import",
"individual_import_button": "Single import",
"invalid_import_type_dialog_title": "Invalid import",
@@ -753,6 +938,10 @@
"name": "Allow Duplicate",
"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": {
"name": "Merge Overlays",
"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.",
"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.",
"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_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",
@@ -2845,6 +3036,21 @@
"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."
},
"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": {
"title": "Auto Open Snaps",
"priority_title": "Auto Open Snaps (Priority)",
@@ -3004,7 +3210,8 @@
"auto_delete_sent_messages": {
"countdown_toast": "Message will be deleted in {time}",
"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": {
"above": "Above",
@@ -3243,6 +3450,9 @@
"cancel": "Cancel",
"documentation": "Documentation"
},
"button": {
"cancel": "Cancel"
},
"common": {
"cancel": "Cancel",
"add": "Add",
@@ -3309,6 +3519,32 @@
"failed_to_fetch_message": "Failed to fetch 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": {
"casual": "Casual",
"formal": "Formal",
@@ -3347,7 +3583,3 @@
"openrouter": "OpenRouter"
}
}

View File

@@ -33,11 +33,26 @@ class LocaleWrapper(
lateinit var loadedLocale: Locale
private fun load(locale: String, pfd: ParcelFileDescriptor) {
loadedLocale = if (locale.contains("_")) {
val split = locale.split("_")
Locale.Builder().setLanguage(split[0]).setRegion(split[1]).build()
} else {
Locale.Builder().setLanguage(locale).build()
loadedLocale = when (locale) {
"zh_SIMPLIFIED" -> Locale.SIMPLIFIED_CHINESE
else -> {
if (locale.contains("_")) {
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 {

View File

@@ -42,7 +42,7 @@ fun Context.getUrlFromClipboard(): String? {
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 {
startActivity(Intent(Intent.ACTION_VIEW).apply {
data = url.toUri()
@@ -50,7 +50,7 @@ fun Context.openLink(url: String, shouldThrow: Boolean = false) {
})
}.onFailure {
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 {
runnable()
}.onFailure {
longToast("Async task failed: " + it.message)
longToast(translation.format("toast_async_task_failed", "message" to (it.message ?: "")))
log.error("Async task failed", it)
}
}
@@ -139,7 +139,7 @@ class ModContext(
fun logCritical(message: Any?, throwable: Throwable = 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({

View File

@@ -198,7 +198,7 @@ class PurrfectSnap {
log.verbose("Features initialized successfully")
}.onFailure { 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
}
@@ -209,7 +209,7 @@ class PurrfectSnap {
log.verbose("Script runtime initialized successfully")
}.onFailure { 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) }
.getOrElse { 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
}
if (outputZip.parentFile == context.androidContext.cacheDir) {
outputZip.delete()
}
context.longToast("Exported to $exportedPath")
context.longToast(
context.translation.format("toast_exported_to_path", "path" to exportedPath)
)
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -863,7 +865,7 @@ class ExportMemories : AbstractAction() {
}.getOrNull()
if (database == null) {
context.longToast("Failed to open memories database")
context.longToast(context.translation["toast_open_memories_db_failed"])
return@launch
}

View File

@@ -87,7 +87,7 @@ class ManageFriendList : AbstractAction() {
private fun addFriend(userId: String) {
val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance
?: run {
context.longToast("Failed to add friend: FriendRelationshipChanger instance not available")
context.longToast(context.translation["toast_friend_add_unavailable"])
return
}
@@ -153,7 +153,9 @@ class ManageFriendList : AbstractAction() {
}
}.onFailure {
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 ->
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(
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()
}.onFailure {
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)

View File

@@ -99,7 +99,12 @@ class DatabaseAccess(
context.log.error("Failed to execute query $query", it)
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.crash("Database ${this.path} is corrupted!", it)
}.getOrNull()
@@ -616,4 +621,4 @@ class DatabaseAccess(
}
}
}
}
}

View File

@@ -164,7 +164,12 @@ class FeatureManager(
}
}.onFailure {
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
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 importRequestCode: Int? = null
@@ -107,7 +108,9 @@ class AccountSwitcher: Feature("Account Switcher") {
onClick = {
runCatching {
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
}
@@ -117,7 +120,7 @@ class AccountSwitcher: Feature("Account Switcher") {
login(userId = user.first, username = user.second)
}.onFailure {
context.shortToast("Failed to login. Check logs for more info.")
context.shortToast(translation["login_failed_toast"])
context.log.error("Failed to login", it)
}
}
@@ -259,7 +262,7 @@ class AccountSwitcher: Feature("Account Switcher") {
private fun logout() {
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()
}
@@ -268,7 +271,7 @@ class AccountSwitcher: Feature("Account Switcher") {
ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.readBytes() }
}
if (accountData == null) {
context.shortToast("Account data not found")
context.shortToast(translation["data_not_found_toast"])
return
}
@@ -312,12 +315,12 @@ class AccountSwitcher: Feature("Account Switcher") {
zipInputStream.close()
} catch (e: Exception) {
context.log.error("Failed to restore account data", e)
context.shortToast("Failed to restore account data")
context.shortToast(translation["restore_failed_toast"])
return
}
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()
}
@@ -390,9 +393,9 @@ class AccountSwitcher: Feature("Account Switcher") {
context.database.getFriendInfo(context.database.myUserId)?.mutableUsername ?: "Unknown username",
getCurrentAccountData()
)
context.shortToast("Account backed up!")
context.shortToast(translation["backup_success_toast"])
}.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)
}
}
@@ -465,11 +468,13 @@ class AccountSwitcher: Feature("Account Switcher") {
it.toParcelFileDescriptor(context.coroutineScope)
)
}
context.shortToast("Imported $username!")
context.shortToast(translation.format("import_success_toast", "username" to username))
updateUsers()
}
}.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)
}
@@ -522,10 +527,10 @@ class AccountSwitcher: Feature("Account Switcher") {
it.copyTo(outputStream)
}
}
context.shortToast("Account exported!")
context.shortToast(translation["export_success_toast"])
}
}.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)
}
}
@@ -539,10 +544,10 @@ class AccountSwitcher: Feature("Account Switcher") {
runCatching {
val accountStorage = context.bridgeClient.getAccountStorage()
if (accountStorage.isAccountExists(context.database.myUserId)) {
accountStorage.removeAccount(context.database.myUserId)
context.shortToast("Removed account due to forced logout")
}
if (accountStorage.isAccountExists(context.database.myUserId)) {
accountStorage.removeAccount(context.database.myUserId)
context.shortToast(translation["forced_logout_toast"])
}
}
return@hook
}

View File

@@ -75,12 +75,22 @@ class EndToEndEncryption : MessagingRuleFeature(
private fun askForKeys(conversationId: String) {
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
}
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
}
@@ -131,7 +141,12 @@ class EndToEndEncryption : MessagingRuleFeature(
private fun handlePublicKeyRequest(conversationId: String, publicKey: ByteArray) {
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
}
warnKeyOverwrite(friendId) {
@@ -151,7 +166,12 @@ class EndToEndEncryption : MessagingRuleFeature(
private fun handleSecretResponse(conversationId: String, secret: ByteArray) {
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
}
warnKeyOverwrite(friendId) {
@@ -561,4 +581,4 @@ class EndToEndEncryption : MessagingRuleFeature(
}
override fun getRuleState() = RuleState.WHITELIST
}
}

View File

@@ -396,7 +396,7 @@ class AutoDeleteSentMessages : MessagingRuleFeature("Auto Delete Sent Messages",
notificationManager.cancel(9999)
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()) {
isPaused = true
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
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) {
appendNotificationText("Failed to send message: $it")
}
@@ -221,7 +226,7 @@ class Notifications : Feature("Notifications") {
onResult = {
if (it != null) {
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 = {
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) {
if (it != null) {
context.log.error("Failed to open snap: $it")
context.shortToast("Failed to open snap")
context.shortToast(context.translation["toast_open_snap_failed"])
}
}
}
}.onFailure {
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)
}

View File

@@ -113,7 +113,7 @@ class ConversationToolbox : Feature("Conversation Toolbox") {
private fun openToolbox() {
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
}

View File

@@ -59,19 +59,19 @@ object LSPatchUpdater {
}
context.log.verbose("updating", TAG)
context.shortToast("Updating PurrfectSnap. Please wait...")
context.shortToast(context.translation["toast_updating_purrfectsnap"])
// copy embedded module to cache
runCatching {
seAppApk.copyTo(embeddedModule, overwrite = true)
}.onFailure {
seAppApk.delete()
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()
return
}
context.longToast("PurrfectSnap updated!")
context.longToast(context.translation["toast_purrfectsnap_updated"])
context.log.verbose("updated", TAG)
context.softRestartApp()
}