This commit is contained in:
ΞTΞRNAL
2026-03-27 01:05:17 +05:30
parent 070a8ffaf7
commit 182e7eefeb
4 changed files with 207 additions and 123 deletions

View File

@@ -11,21 +11,14 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@@ -34,15 +27,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
import me.eternal.purrfectsnap.ui.setup.screens.SetupScreen import me.eternal.purrfectsnap.ui.setup.screens.SetupScreen
import me.eternal.purrfectsnap.ui.util.AlertDialogs import me.eternal.purrfectsnap.ui.util.AlertDialogs
@@ -55,16 +44,7 @@ class MappingsScreen : SetupScreen() {
val translation = context.translation val translation = context.translation
var infoText by remember { mutableStateOf(null as String?) } var infoText by remember { mutableStateOf(null as String?) }
var isGenerating by remember { mutableStateOf(false) } var isGenerating by remember { mutableStateOf(false) }
var showCompletionNotice by remember { mutableStateOf(false) } fun finishMappings() = goNext()
var completionCountdown by remember { mutableIntStateOf(10) }
fun finishMappings() {
if (isFirstRunFlow) {
showCompletionNotice = true
} else {
goNext()
}
}
if (infoText != null) { if (infoText != null) {
fun dismiss() { fun dismiss() {
@@ -87,103 +67,6 @@ class MappingsScreen : SetupScreen() {
} }
} }
LaunchedEffect(showCompletionNotice) {
if (showCompletionNotice) {
completionCountdown = 10
while (completionCountdown > 0) {
delay(1000)
completionCountdown--
}
}
}
if (showCompletionNotice) {
val confirmLabel = if (completionCountdown > 0) {
translation.format(
"setup.mappings.confirm_understand_timeout",
"seconds" to completionCountdown.toString()
)
} else {
translation["setup.mappings.confirm_understand"]
}
AestheticDialog(
onDismissRequest = { if (completionCountdown == 0) { showCompletionNotice = false; goNext() } },
title = translation["setup.mappings.notice_title"],
text = "",
icon = Icons.Filled.Warning,
confirmButtonText = confirmLabel,
onConfirm = { if (completionCountdown == 0) { showCompletionNotice = false; goNext() } },
confirmEnabled = completionCountdown == 0,
showCloseButton = false,
customContent = {
val bodyStyle = MaterialTheme.typography.bodyMedium.copy(
color = PurrfectPalette.textSecondary,
lineHeight = 18.sp
)
Surface(
shape = RoundedCornerShape(14.dp),
color = PurrfectPalette.cardOverlayColor,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
border = BorderStroke(
1.dp,
Brush.linearGradient(
listOf(
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
)
)
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 360.dp)
.verticalScroll(rememberScrollState())
.padding(horizontal = 14.dp, vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(
text = translation["setup.mappings.notice_intro"],
style = bodyStyle,
modifier = Modifier.fillMaxWidth()
)
Text(
text = translation["setup.mappings.notice_step_1"],
style = bodyStyle,
modifier = Modifier.fillMaxWidth()
)
Text(
text = translation["setup.mappings.notice_step_2"],
style = bodyStyle,
modifier = Modifier.fillMaxWidth()
)
Text(
text = translation["setup.mappings.notice_step_3"],
style = bodyStyle,
modifier = Modifier.fillMaxWidth()
)
Text(
text = translation["setup.mappings.notice_rooted_title"],
style = MaterialTheme.typography.bodyMedium.copy(
color = Color.White,
fontWeight = FontWeight.SemiBold,
lineHeight = 18.sp
),
modifier = Modifier.fillMaxWidth()
)
Text(
text = translation["setup.mappings.notice_rooted_body"],
style = bodyStyle,
modifier = Modifier.fillMaxWidth()
)
}
}
}
)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
if (isGenerating) return@launch if (isGenerating) return@launch

View File

@@ -33,8 +33,8 @@ tasks.register<GetVersionTask>("getVersion") {
} }
// You can still set these for legacy use by submodules or scripts: // You can still set these for legacy use by submodules or scripts:
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.5.5").get()) rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.5.6").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("302").get().toInt()) rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("304").get().toInt())
rootProject.ext.set("applicationId", "me.eternal.purrfectsnap") rootProject.ext.set("applicationId", "me.eternal.purrfectsnap")
// buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate. // buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate.
// Include version code so each release has a different hash; use random for uniqueness within same version. // Include version code so each release has a different hash; use random for uniqueness within same version.

View File

@@ -2,21 +2,36 @@ package me.eternal.purrfectsnap.core
import android.system.Os import android.system.Os
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.outlined.Cancel import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.rounded.NotInterested import androidx.compose.material.icons.rounded.NotInterested
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.ui.window.Dialog
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import me.eternal.purrfectsnap.common.bridge.FileHandleScope import me.eternal.purrfectsnap.common.bridge.FileHandleScope
import me.eternal.purrfectsnap.common.bridge.toWrapper import me.eternal.purrfectsnap.common.bridge.toWrapper
@@ -25,6 +40,8 @@ import me.eternal.purrfectsnap.common.config.VersionRequirement
import me.eternal.purrfectsnap.common.ui.createComposeView import me.eternal.purrfectsnap.common.ui.createComposeView
import me.eternal.purrfectsnap.core.event.events.impl.UnaryCallEvent import me.eternal.purrfectsnap.core.event.events.impl.UnaryCallEvent
import me.eternal.purrfectsnap.core.ui.CustomComposable import me.eternal.purrfectsnap.core.ui.CustomComposable
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayPalette
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayTheme
import me.eternal.purrfectsnap.core.util.dataBuilder import me.eternal.purrfectsnap.core.util.dataBuilder
import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.HookStage
import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hook
@@ -48,6 +65,162 @@ class SecurityFeatures(
transact(this, 0)?.toString(2)?.padStart(32, '0')?.count { it == '1' } transact(this, 0)?.toString(2)?.padStart(32, '0')?.count { it == '1' }
} }
private fun isLoginSignupActivity() = context.mainActivity?.javaClass?.name?.endsWith("LoginSignupActivity") == true
@Composable
private fun LoginSignupHelpButton(onClick: () -> Unit) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 16.dp),
contentAlignment = Alignment.TopCenter
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.clip(RoundedCornerShape(999.dp))
.background(
Brush.horizontalGradient(
listOf(
PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.92f),
PurrfectOverlayPalette.glowSecondary.copy(alpha = 0.62f)
)
),
RoundedCornerShape(999.dp)
)
.border(
BorderStroke(1.dp, PurrfectOverlayPalette.textPrimary.copy(alpha = 0.18f)),
RoundedCornerShape(999.dp)
)
.clickable(onClick = onClick)
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(Color.Black.copy(alpha = 0.16f))
.border(BorderStroke(1.dp, PurrfectOverlayPalette.textPrimary.copy(alpha = 0.28f)), CircleShape)
) {
Icon(
imageVector = Icons.Filled.HelpOutline,
contentDescription = "Login Help",
tint = Color.White,
modifier = Modifier.size(18.dp)
)
}
Text(
text = "Can't Login?",
color = Color.White,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp
)
}
}
}
@Composable
private fun LoginSignupHelpDialog(onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
PurrfectOverlayTheme {
val shape = RoundedCornerShape(20.dp)
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = shape,
color = Color.Transparent,
border = BorderStroke(
1.dp,
Brush.linearGradient(
listOf(
PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.55f),
PurrfectOverlayPalette.glowSecondary.copy(alpha = 0.35f)
)
)
),
shadowElevation = 0.dp,
tonalElevation = 0.dp
) {
Box(
modifier = Modifier
.background(PurrfectOverlayPalette.cardOverlay, shape)
.padding(20.dp)
) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
Icon(
Icons.Filled.Info,
contentDescription = null,
tint = PurrfectOverlayPalette.textPrimary,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(28.dp)
)
Text(
text = context.translation["setup.mappings.notice_title"],
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = PurrfectOverlayPalette.textPrimary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Text(
text = context.translation["setup.mappings.notice_intro"],
color = PurrfectOverlayPalette.textSecondary,
textAlign = TextAlign.Start
)
Text(
text = "For non-rooted users:",
color = PurrfectOverlayPalette.textPrimary,
fontWeight = FontWeight.SemiBold
)
Text(
text = context.translation["setup.mappings.notice_step_1"],
color = PurrfectOverlayPalette.textSecondary,
textAlign = TextAlign.Start
)
Text(
text = context.translation["setup.mappings.notice_step_2"],
color = PurrfectOverlayPalette.textSecondary,
textAlign = TextAlign.Start
)
Text(
text = context.translation["setup.mappings.notice_step_3"],
color = PurrfectOverlayPalette.textSecondary,
textAlign = TextAlign.Start
)
Text(
text = context.translation["setup.mappings.notice_rooted_title"],
color = PurrfectOverlayPalette.textPrimary,
fontWeight = FontWeight.SemiBold
)
Text(
text = context.translation["setup.mappings.notice_rooted_body"],
color = PurrfectOverlayPalette.textSecondary,
textAlign = TextAlign.Start
)
Button(
onClick = onDismiss,
colors = ButtonDefaults.buttonColors(
containerColor = PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.92f),
contentColor = Color.Black
),
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text("OK")
}
}
}
}
}
}
}
fun init() { fun init() {
val snapchatVersionCode = context.androidContext.packageManager?.getPackageInfo(context.androidContext.packageName, 0)?.longVersionCode ?: throw IllegalStateException("Failed to get version code") val snapchatVersionCode = context.androidContext.packageManager?.getPackageInfo(context.androidContext.packageName, 0)?.longVersionCode ?: throw IllegalStateException("Failed to get version code")
var shouldDisablePlugin = MOD_DETECTION_VERSION_CHECK.checkVersion(snapchatVersionCode)?.second == VersionRequirement.OLDER_REQUIRED var shouldDisablePlugin = MOD_DETECTION_VERSION_CHECK.checkVersion(snapchatVersionCode)?.second == VersionRequirement.OLDER_REQUIRED
@@ -101,6 +274,34 @@ class SecurityFeatures(
} }
} }
lateinit var loginHelpComposable: CustomComposable
loginHelpComposable = {
var showDialog by remember { mutableStateOf(false) }
var isLoginScreen by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
while (true) {
val currentlyInLogin = isLoginSignupActivity()
isLoginScreen = currentlyInLogin
if (!currentlyInLogin) showDialog = false
delay(150)
}
}
if (isLoginScreen) {
LoginSignupHelpButton(
onClick = { showDialog = true }
)
}
if (isLoginScreen && showDialog) {
LoginSignupHelpDialog(
onDismiss = { showDialog = false }
)
}
}
context.inAppOverlay.addCustomComposable(loginHelpComposable)
if (!context.disablePlugin) return if (!context.disablePlugin) return
val allowedEPs = listOf( val allowedEPs = listOf(

View File

@@ -7,8 +7,8 @@ org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn org.gradle.configuration-cache.problems=warn
nativeAbis=arm64-v8a nativeAbis=arm64-v8a
APP_VERSION_NAME=1.5.5 APP_VERSION_NAME=1.5.6
APP_VERSION_CODE=302 APP_VERSION_CODE=304
debug_build_hash=18fe2a814d0e2eb5 debug_build_hash=18fe2a814d0e2eb5
psIntegrityPinnedSha256= psIntegrityPinnedSha256=
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c