2 Commits

Author SHA1 Message Date
ΞTΞRNAL
565a8b8287 v1.4.9 2026-03-21 03:48:43 +05:30
ΞTΞRNAL
1c612ebc8e fix: force AMOLED theme for newer snap versions 2026-03-21 02:09:33 +05:30
7 changed files with 184 additions and 46 deletions

View File

@@ -33,8 +33,8 @@ tasks.register<GetVersionTask>("getVersion") {
}
// You can still set these for legacy use by submodules or scripts:
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.4.8").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("288").get().toInt())
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.4.9").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("290").get().toInt())
rootProject.ext.set("applicationId", "me.eternal.purrfectsnap")
// 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.

View File

@@ -1,3 +1,7 @@
## v1.4.9
- Fix: Force AMOLED Theme for newer versions of Snapchat
- New: Redesign some dialogs(Call confirmation & Mark Snaps as seen)
## v1.4.8
- Fix: Crash Issues for some devices
- Fix: Crash when using the theme button in android 11 & 12(tq to Kaladin)

View File

@@ -1,18 +1,31 @@
package me.eternal.purrfectsnap.core.features.impl.messaging
import android.widget.ProgressBar
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.WarningAmber
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog
import me.eternal.purrfectsnap.common.data.ContentType
import me.eternal.purrfectsnap.common.data.MessageUpdate
import me.eternal.purrfectsnap.core.event.events.impl.OnSnapInteractionEvent
import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent
import me.eternal.purrfectsnap.core.features.Feature
import me.eternal.purrfectsnap.core.features.impl.spying.StealthMode
import me.eternal.purrfectsnap.core.ui.PurrfectGlassCard
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayPalette
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayTheme
import me.eternal.purrfectsnap.core.ui.ViewAppearanceHelper
import me.eternal.purrfectsnap.core.util.CallbackBuilder
import me.eternal.purrfectsnap.core.util.hook.HookStage
@@ -61,20 +74,41 @@ class AutoMarkAsRead : Feature("Auto Mark As Read") {
}
var job: Job? = null
val dialog = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
.setTitle("Processing...")
.setView(ProgressBar(context.mainActivity).apply {
setPadding(10, 10, 10, 10)
})
.setOnDismissListener { job?.cancel() }
.show()
val processedCount = mutableIntStateOf(0)
val dialog = createComposeAlertDialog(context.mainActivity!!, builder = {
setOnDismissListener { job?.cancel() }
}) {
PurrfectOverlayTheme {
PurrfectGlassCard(
modifier = androidx.compose.ui.Modifier.fillMaxWidth(),
title = "Marking Snaps as Seen",
subtitle = "Updating read state for queued snaps",
icon = Icons.Default.Visibility
) {
Column(
modifier = androidx.compose.ui.Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally
) {
CircularProgressIndicator(
color = PurrfectOverlayPalette.glowSecondary,
trackColor = PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.18f)
)
Text(
text = "${processedCount.intValue}/${messageIds.size}",
color = PurrfectOverlayPalette.textSecondary
)
}
}
}
}.apply { show() }
context.coroutineScope.launch(Dispatchers.IO) {
messageIds.forEach { messageId ->
messageIds.forEachIndexed { index, messageId ->
markSnapAsSeen(conversationId, messageId)
delay(Random.nextLong(20, 60))
context.runOnUiThread {
dialog.setTitle("Processing... (${messageIds.indexOf(messageId) + 1}/${messageIds.size})")
processedCount.intValue = index + 1
}
}
}.also { job = it }.invokeOnCompletion {
@@ -153,4 +187,4 @@ class AutoMarkAsRead : Feature("Auto Mark As Read") {
}
}
}
}
}

View File

@@ -3,8 +3,28 @@ package me.eternal.purrfectsnap.core.features.impl.messaging
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Call
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog
import me.eternal.purrfectsnap.core.event.events.impl.AddViewEvent
import me.eternal.purrfectsnap.core.features.Feature
import me.eternal.purrfectsnap.core.ui.PurrfectGlassCard
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayPalette
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayTheme
import me.eternal.purrfectsnap.core.ui.ViewAppearanceHelper
import me.eternal.purrfectsnap.core.ui.children
import me.eternal.purrfectsnap.core.ui.hideViewCompletely
@@ -16,12 +36,46 @@ class CallButtonsOverride : Feature("CallButtonsOverride") {
private fun hookTouchEvent(param: HookAdapter, motionEvent: MotionEvent, onConfirm: () -> Unit) {
if (motionEvent.action != MotionEvent.ACTION_UP) return
param.setResult(true)
ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
.setTitle(context.translation["call_start_confirmation.dialog_title"])
.setMessage(context.translation["call_start_confirmation.dialog_message"])
.setPositiveButton(context.translation["button.positive"]) { _, _ -> onConfirm() }
.setNeutralButton(context.translation["button.negative"]) { _, _ -> }
.show()
createComposeAlertDialog(context.mainActivity!!) { alertDialog ->
PurrfectOverlayTheme {
PurrfectGlassCard(
modifier = Modifier.fillMaxWidth(),
title = context.translation["call_start_confirmation.dialog_title"],
subtitle = context.translation["call_start_confirmation.dialog_message"],
icon = Icons.Default.Call
) {
val actionShape = RoundedCornerShape(16.dp)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp, androidx.compose.ui.Alignment.CenterHorizontally)
) {
Button(
modifier = Modifier.width(120.dp),
onClick = { alertDialog.dismiss() },
colors = ButtonDefaults.buttonColors(
containerColor = Color.White.copy(alpha = 0.08f),
contentColor = Color.White
)
) {
Text(context.translation["button.negative"])
}
Button(
modifier = Modifier.width(120.dp),
onClick = {
alertDialog.dismiss()
onConfirm()
},
colors = ButtonDefaults.buttonColors(
containerColor = PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.26f),
contentColor = Color.White
)
) {
Text(context.translation["button.positive"])
}
}
}
}
}.show()
}
override fun init() {

View File

@@ -1,10 +1,13 @@
package me.eternal.purrfectsnap.core.features.impl.ui
import android.content.res.TypedArray
import android.graphics.drawable.ColorDrawable
import android.util.TypedValue
import android.view.View
import me.eternal.purrfectsnap.core.features.Feature
import me.eternal.purrfectsnap.core.util.hook.HookStage
import me.eternal.purrfectsnap.core.util.hook.hook
import me.eternal.purrfectsnap.core.util.hook.hookConstructor
import me.eternal.purrfectsnap.core.util.ktx.getObjectField
class CustomTheming : Feature("Custom Theming") {
@@ -50,37 +53,80 @@ class CustomTheming : Feature("Custom Theming") {
).any { it in name }
}
private fun patchTypedArray(result: TypedArray, attrIds: IntArray?) {
val requestedAttrs = attrIds?.takeIf { it.isNotEmpty() } ?: return
val typedArrayData = runCatching { result.getObjectField("mData") as IntArray }.getOrNull() ?: return
val stride = (typedArrayData.size / requestedAttrs.size).takeIf { it >= 2 } ?: return
requestedAttrs.forEachIndexed { index, attrId ->
val offset = index * stride
if (offset + 1 >= typedArrayData.size) return@forEachIndexed
val type = typedArrayData[offset]
if (type !in colorTypes) return@forEachIndexed
val originalColor = runCatching { result.getColor(index, Int.MIN_VALUE) }.getOrNull()
?.takeIf { it != Int.MIN_VALUE }
?: return@forEachIndexed
val attrName = runCatching { context.androidContext.resources.getResourceEntryName(attrId) }.getOrNull()
val shouldPatch = attrId in patchedAttrIds || shouldPatch(attrId, attrName, originalColor)
if (!shouldPatch) return@forEachIndexed
typedArrayData[offset + 1] = amoledBlack
if (patchedAttrIds.add(attrId)) {
context.log.verbose(
"[AMOLED PATCH] Patched attrId 0x${attrId.toString(16)} (${attrName ?: "unknown"}) from 0x${originalColor.toUInt().toString(16)} to AMOLED black"
)
}
}
}
private fun patchProgrammaticColor(color: Int): Int {
return if (isNearBlackOpaque(color) && color != amoledBlack) amoledBlack else color
}
override fun init() {
if (!context.config.userInterface.forceAmoledTheme.get()) return
onNextActivityCreate {
context.androidContext.theme.javaClass
.getMethod("obtainStyledAttributes", IntArray::class.java)
.hook(HookStage.AFTER) { param ->
val array = param.arg<IntArray>(0)
val attrId = array[0]
val result = param.getResult() as TypedArray
val type = result.getType(0)
if (type !in colorTypes) return@hook
val originalColor = runCatching { result.getColor(0, Int.MIN_VALUE) }.getOrNull()
?.takeIf { it != Int.MIN_VALUE }
?: return@hook
val attrName = runCatching { context.androidContext.resources.getResourceEntryName(attrId) }.getOrNull()
val shouldPatch = attrId in patchedAttrIds || shouldPatch(attrId, attrName, originalColor)
if (!shouldPatch) return@hook
val typedArrayData = runCatching { result.getObjectField("mData") as IntArray }.getOrNull() ?: return@hook
if (typedArrayData.size < 2) return@hook
typedArrayData[1] = amoledBlack
if (patchedAttrIds.add(attrId)) {
context.log.verbose(
"[AMOLED PATCH] Patched attrId 0x${attrId.toString(16)} (${attrName ?: "unknown"}) from 0x${originalColor.toUInt().toString(16)} to AMOLED black"
)
}
.hook("obtainStyledAttributes", HookStage.AFTER) { param ->
val requestedAttrs = param.args().firstOrNull { it is IntArray } as? IntArray
val result = param.getResult() as? TypedArray ?: return@hook
patchTypedArray(result, requestedAttrs)
}
context.androidContext.javaClass
.hook("obtainStyledAttributes", HookStage.AFTER) { param ->
val requestedAttrs = param.args().firstOrNull { it is IntArray } as? IntArray
val result = param.getResult() as? TypedArray ?: return@hook
patchTypedArray(result, requestedAttrs)
}
View::class.java.hook("setBackgroundColor", HookStage.BEFORE) { param ->
val color = param.argNullable<Int>(0) ?: return@hook
val patched = patchProgrammaticColor(color)
if (patched != color) {
param.setArg(0, patched)
}
}
ColorDrawable::class.java.hookConstructor(HookStage.BEFORE) { param ->
val color = param.argNullable<Int>(0) ?: return@hookConstructor
val patched = patchProgrammaticColor(color)
if (patched != color) {
param.setArg(0, patched)
}
}
ColorDrawable::class.java.hook("setColor", HookStage.BEFORE) { param ->
val color = param.argNullable<Int>(0) ?: return@hook
val patched = patchProgrammaticColor(color)
if (patched != color) {
param.setArg(0, patched)
}
}
}
}
}

View File

@@ -7,8 +7,8 @@ org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
nativeAbis=arm64-v8a
APP_VERSION_NAME=1.4.8
APP_VERSION_CODE=288
APP_VERSION_NAME=1.4.9
APP_VERSION_CODE=290
debug_build_hash=18fe2a814d0e2eb5
psIntegrityPinnedSha256=
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c