feat: Spoof Followers Count by RSR (#142)
This commit is contained in:
@@ -431,9 +431,10 @@ class AlertDialogs(
|
||||
DefaultDialogCard {
|
||||
var fieldValue by remember {
|
||||
mutableStateOf(property.value.get().toString().let {
|
||||
val t = if (property.key.params.digitsOnlyInput) it.filter { ch -> ch.isDigit() } else it
|
||||
TextFieldValue(
|
||||
text = it,
|
||||
selection = TextRange(it.length)
|
||||
text = t,
|
||||
selection = TextRange(t.length)
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -447,10 +448,21 @@ class AlertDialogs(
|
||||
}
|
||||
.focusRequester(focusRequester),
|
||||
value = fieldValue,
|
||||
onValueChange = { fieldValue = it },
|
||||
keyboardOptions = when (property.key.dataType.type) {
|
||||
DataProcessors.Type.INTEGER -> KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
DataProcessors.Type.FLOAT -> KeyboardOptions(keyboardType = KeyboardType.Decimal)
|
||||
onValueChange = { newVal ->
|
||||
fieldValue = if (property.key.params.digitsOnlyInput) {
|
||||
val filtered = newVal.text.filter { ch -> ch.isDigit() }
|
||||
if (newVal.text != filtered) {
|
||||
Toast.makeText(context, translation["manager.sections.features.digits_only_toast"], Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
newVal.copy(text = filtered)
|
||||
} else {
|
||||
newVal
|
||||
}
|
||||
},
|
||||
keyboardOptions = when {
|
||||
property.key.params.digitsOnlyInput -> KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
property.key.dataType.type == DataProcessors.Type.INTEGER -> KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
property.key.dataType.type == DataProcessors.Type.FLOAT -> KeyboardOptions(keyboardType = KeyboardType.Decimal)
|
||||
else -> KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
},
|
||||
singleLine = true,
|
||||
|
||||
@@ -359,7 +359,8 @@
|
||||
"search_button": "بحث",
|
||||
"search_results_count": "{count} رسالة",
|
||||
"clear_history": "مسح سجل البحث",
|
||||
"subtitle": "البحث وإدارة الميزات"
|
||||
"subtitle": "البحث وإدارة الميزات",
|
||||
"digits_only_toast": "يُسمح بالأرقام فقط."
|
||||
},
|
||||
"bypass_status": {
|
||||
"active": "PurrAura نشط",
|
||||
@@ -1169,6 +1170,16 @@
|
||||
"description": "تعيين نقاط السناب شات الوهمية (أقصى عدد هو 9,999,999)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"spoof_followers_count": {
|
||||
"name": "تزييف عدد المتابعين",
|
||||
"description": "يعرض عدد متابعين مزيّفاً على ملفك (محلي فقط).",
|
||||
"properties": {
|
||||
"custom_followers_count": {
|
||||
"name": "عدد المتابعين المخصص",
|
||||
"description": "الرقم المعروض (أرقام فقط)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2569,9 +2580,11 @@
|
||||
"encryption_indicator": "يضيف أيقونة \ud83d\udd12 بجوار الرسائل التي تم إرسالها إليك فقط",
|
||||
"platform_indicator": "يضيف أيقونة المنصة التي تم إرسال الوسيط منها (مثل Android، iOS، Web)",
|
||||
"location_indicator": "يضيف أيقونة \ud83d\udccd للـ snaps عندما يتم إرسالها مع تمكين الموقع",
|
||||
"live_camera_indicator": "إضافة رمز \uD83D\uDCF7 عندما يُلتقط السناب بكاميرا التطبيق مباشرة (تصوير حي)",
|
||||
"external_media_indicator": "إضافة رمز \uD83D\uDDBC\uFE0F لوسائط معرض الصور أو السنابات المُرسَلة من المعرض",
|
||||
"ovf_editor_indicator": "يشير إلى ما إذا كان snap قد تم إرساله باستخدام محرر OVF",
|
||||
"director_mode_indicator": "يضيف أيقونة \u270f\ufe0f للـ snaps عندما يتم إرسالها باستخدام وضع المخرج، والذي يمكن استخدامه لإرسال صور المعرض كـ snaps",
|
||||
"memories_indicator": "إضافة رمز \uD83D\uDCD6 للسنابات التي تم إعادة إرسالها من الذكريات بدلاً من التقاطها بالكاميرا الحية"
|
||||
"memories_indicator": "إضافة أيقونة كتاب مفتوح (\uD83D\uDCD6) عند إعادة إرسال سناب من الذكريات وليس بالتقاط حي بالكاميرا"
|
||||
},
|
||||
"auto_mark_as_read": {
|
||||
"conversation_read": "وضع علامة مقروء على المحادثة عند إرسال رسالة",
|
||||
|
||||
@@ -391,7 +391,8 @@
|
||||
"search_button": "Search",
|
||||
"search_results_count": "{count} messages",
|
||||
"clear_history": "Clear search history",
|
||||
"subtitle": "Explore and manage premium features"
|
||||
"subtitle": "Explore and manage premium features",
|
||||
"digits_only_toast": "Only numbers are allowed."
|
||||
},
|
||||
"bypass_status": {
|
||||
"active": "PurrAura Active",
|
||||
@@ -1277,6 +1278,16 @@
|
||||
"description": "The custom Snap Score you want to display (max 9,999,999)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"spoof_followers_count": {
|
||||
"name": "Spoof Followers Count",
|
||||
"description": "Spoof your follower count on your profile (local only).",
|
||||
"properties": {
|
||||
"custom_followers_count": {
|
||||
"name": "Custom Followers Count",
|
||||
"description": "Number to show (digits only)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3256,9 +3267,11 @@
|
||||
"encryption_indicator": "Adds a \ud83d\udd12 icon next to messages that have been sent only to you",
|
||||
"platform_indicator": "Adds the platform icon from which a media was sent (e.g. Android, iOS, Web)",
|
||||
"location_indicator": "Adds a \ud83d\udccd icon to snaps when they have been sent with location enabled",
|
||||
"live_camera_indicator": "Adds a \ud83d\dcf7 icon when the snap was captured with the in-app camera (live)",
|
||||
"external_media_indicator": "Adds a \ud83d\uddbc\ufe0f icon for camera roll / gallery media and snaps sent from the gallery",
|
||||
"ovf_editor_indicator": "Indicates if a snap has been sent using OVF Editor",
|
||||
"director_mode_indicator": "Adds a \u270f\ufe0f icon to snaps when they have been sent using Director Mode, which can be used to send gallery images as snaps",
|
||||
"memories_indicator": "Adds a \ud83d\udcd6 icon to snaps that were re-sent from Memories instead of being captured with the live camera"
|
||||
"memories_indicator": "Adds an open-book style icon (\ud83d\udcd6) when a snap was re-sent from Memories (not live capture)"
|
||||
},
|
||||
"auto_mark_as_read": {
|
||||
"conversation_read": "Mark conversation as read when sending a message",
|
||||
|
||||
@@ -81,6 +81,7 @@ class ConfigParams(
|
||||
var inputCheck: ((String) -> Boolean)? = { true },
|
||||
var filenameFilter: ((String) -> Boolean)? = null,
|
||||
var versionCheck: VersionCheck? = null,
|
||||
var digitsOnlyInput: Boolean = false,
|
||||
) {
|
||||
val notices get() = _notices?.let { FeatureNotice.entries.filter { flag -> it and flag.id != 0 } } ?: emptyList()
|
||||
val flags get() = _flags?.let { ConfigFlag.entries.filter { flag -> it and flag.id != 0 } } ?: emptyList()
|
||||
|
||||
@@ -57,7 +57,7 @@ class UserInterfaceTweaks : ConfigContainer() {
|
||||
val oldBitmojiSelfie = unique("old_bitmoji_selfie", "2d", "3d") { requireCleanCache() }
|
||||
val disableSpotlight = boolean("disable_spotlight") { requireRestart() }
|
||||
val verticalStoryViewer = boolean("vertical_story_viewer") { requireRestart() }
|
||||
val messageIndicators = multiple("message_indicators", "encryption_indicator", "platform_indicator", "location_indicator", "ovf_editor_indicator", "director_mode_indicator", "memories_indicator") { requireRestart() }
|
||||
val messageIndicators = multiple("message_indicators", "encryption_indicator", "platform_indicator", "location_indicator", "live_camera_indicator", "external_media_indicator", "ovf_editor_indicator", "director_mode_indicator", "memories_indicator") { requireRestart() }
|
||||
val stealthModeIndicator = boolean("stealth_mode_indicator") { requireRestart() }
|
||||
val editTextOverride = multiple("edit_text_override", "multi_line_chat_input", "bypass_text_input_limit") {
|
||||
requireRestart(); addNotices(FeatureNotice.BAN_RISK, FeatureNotice.INTERNAL_BEHAVIOR)
|
||||
@@ -77,4 +77,14 @@ class UserInterfaceTweaks : ConfigContainer() {
|
||||
}
|
||||
|
||||
val spoofSnapScore = container("spoof_snap_score", SpoofSnapScore()) { requireRestart() }
|
||||
|
||||
inner class SpoofFollowersCount : ConfigContainer(hasGlobalState = true) {
|
||||
val customFollowersCount = string("custom_followers_count") {
|
||||
requireRestart()
|
||||
digitsOnlyInput = true
|
||||
inputCheck = { input -> input.isEmpty() || input.all { it.isDigit() } }
|
||||
}
|
||||
}
|
||||
|
||||
val spoofFollowersCount = container("spoof_followers_count", SpoofFollowersCount()) { requireRestart() }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package me.eternal.purrfectsnap.core.features.impl.ui
|
||||
|
||||
import android.widget.TextView
|
||||
import me.eternal.purrfectsnap.core.features.Feature
|
||||
import me.eternal.purrfectsnap.core.util.hook.HookStage
|
||||
import me.eternal.purrfectsnap.core.util.hook.hook
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.math.MathContext
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
class FakeFollowersCount : Feature("Fake Followers Count") {
|
||||
|
||||
override fun init() {
|
||||
if (context.config.userInterface.spoofFollowersCount.globalState != true) return
|
||||
val raw = context.config.userInterface.spoofFollowersCount.customFollowersCount.getNullable()?.trim()?.takeIf { it.isNotBlank() }
|
||||
?: return
|
||||
val digits = raw.replace(Regex("[^0-9]"), "")
|
||||
if (digits.isEmpty()) return
|
||||
val followerValue = try {
|
||||
BigInteger(digits)
|
||||
} catch (_: NumberFormatException) {
|
||||
return
|
||||
}
|
||||
|
||||
onNextActivityCreate {
|
||||
TextView::class.java.hook("setText", HookStage.BEFORE) { param ->
|
||||
val textView = param.thisObject() as? TextView ?: return@hook
|
||||
if (textView.javaClass.name !in COMPOSER_SNAP_TEXT_VIEW) return@hook
|
||||
val arg0 = param.argNullable<Any>(0) ?: return@hook
|
||||
if (arg0 !is CharSequence) return@hook
|
||||
val text = arg0.toString()
|
||||
if (!FOLLOWERS_LINE.containsMatchIn(text)) return@hook
|
||||
val display = formatFollowersDisplay(followerValue)
|
||||
val replaced = FOLLOWERS_LINE.replace(text) { mr ->
|
||||
"$display ${mr.groupValues[2]}"
|
||||
}
|
||||
param.setArg(0, replaced)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatFollowersDisplay(n: BigInteger): String {
|
||||
val v = n.max(BigInteger.ZERO)
|
||||
if (v < TEN_THOUSAND) {
|
||||
return NumberFormat.getIntegerInstance(Locale.US).format(v.toLong())
|
||||
}
|
||||
val (scaled, suffix) = scaleToTier(v)
|
||||
return formatMantissaMaxFourDigits(scaled) + suffix
|
||||
}
|
||||
|
||||
private fun scaleToTier(v: BigInteger): Pair<BigDecimal, String> {
|
||||
var i = SCALE_TIERS.indexOfLast { v >= it.floor }.coerceAtLeast(0)
|
||||
var floor = SCALE_TIERS[i].floor
|
||||
var suffix = SCALE_TIERS[i].suffix
|
||||
var scaled = BigDecimal(v, MATH_CTX).divide(BigDecimal(floor, MATH_CTX), MATH_CTX)
|
||||
while (scaled >= THOUSAND_BD && i + 1 < SCALE_TIERS.size) {
|
||||
i++
|
||||
floor = SCALE_TIERS[i].floor
|
||||
suffix = SCALE_TIERS[i].suffix
|
||||
scaled = BigDecimal(v, MATH_CTX).divide(BigDecimal(floor, MATH_CTX), MATH_CTX)
|
||||
}
|
||||
return Pair(scaled, suffix)
|
||||
}
|
||||
|
||||
private fun formatMantissaMaxFourDigits(scaled: BigDecimal): String {
|
||||
val x = scaled.abs().setScale(12, RoundingMode.HALF_UP)
|
||||
if (x.compareTo(BigDecimal.ZERO) == 0) return "0"
|
||||
if (x >= HUNDRED) {
|
||||
val i = x.setScale(0, RoundingMode.HALF_UP)
|
||||
var s = i.toPlainString()
|
||||
if (digitCount(s) > 4) {
|
||||
s = x.round(MathContext(4, RoundingMode.HALF_UP)).setScale(0, RoundingMode.HALF_UP).toPlainString()
|
||||
}
|
||||
return s
|
||||
}
|
||||
if (x >= TEN) {
|
||||
var s = stripFrac(x.setScale(1, RoundingMode.HALF_UP))
|
||||
if (digitCount(s) > 4) {
|
||||
s = x.setScale(0, RoundingMode.HALF_UP).toPlainString()
|
||||
}
|
||||
return s
|
||||
}
|
||||
var s = stripFrac(x.setScale(2, RoundingMode.HALF_UP))
|
||||
if (digitCount(s) > 4) {
|
||||
s = stripFrac(x.setScale(1, RoundingMode.HALF_UP))
|
||||
}
|
||||
if (digitCount(s) > 4) {
|
||||
s = x.setScale(0, RoundingMode.HALF_UP).toPlainString()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
private fun digitCount(s: String) = s.count { it.isDigit() }
|
||||
|
||||
private fun stripFrac(d: BigDecimal): String {
|
||||
var s = d.stripTrailingZeros().toPlainString()
|
||||
if ('.' in s) {
|
||||
s = s.trimEnd('0').trimEnd('.')
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
private data class ScaleTier(val floor: BigInteger, val suffix: String)
|
||||
|
||||
companion object {
|
||||
private val FOLLOWERS_LINE = Regex("^([\\d,]+)\\s+(Followers)\\b", RegexOption.IGNORE_CASE)
|
||||
private val COMPOSER_SNAP_TEXT_VIEW = setOf(
|
||||
"com.snap.valdi.views.ComposerSnapTextView",
|
||||
"com.snap.composer.views.ComposerSnapTextView",
|
||||
)
|
||||
private val MATH_CTX = MathContext(24, RoundingMode.HALF_UP)
|
||||
private val TEN_THOUSAND = BigInteger("10000")
|
||||
private val THOUSAND_BD = BigDecimal("1000")
|
||||
private val HUNDRED = BigDecimal("100")
|
||||
private val TEN = BigDecimal("10")
|
||||
private val SCALE_TIERS = listOf(
|
||||
ScaleTier(BigInteger("1000"), "K"),
|
||||
ScaleTier(BigInteger("1000000"), "M"),
|
||||
ScaleTier(BigInteger("1000000000"), "B"),
|
||||
ScaleTier(BigInteger("1000000000000"), "T"),
|
||||
ScaleTier(BigInteger("1000000000000000"), "Q"),
|
||||
ScaleTier(BigInteger("1000000000000000000"), "E"),
|
||||
ScaleTier(BigInteger("1000000000000000000000"), "Z"),
|
||||
ScaleTier(BigInteger("1000000000000000000000000"), "Y"),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user