commit c44c36dbd4e9970a9054bf1730fcbd9597da5704
parent 68c61a0b73f70f2c508d4dbc28eaf02ff3c46c46
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu, 18 Apr 2024 00:59:17 +0200

feat(experimental): custom streaks expiration format

Diffstat:
Mcommon/src/main/assets/lang/en_US.json | 4++++
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt | 1+
Acore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomStreaksExpirationFormat.kt | 44++++++++++++++++++++++++++++++++++++++++++++
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt | 1+
Amapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StreaksExpirationMapper.kt | 48++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 99 insertions(+), 0 deletions(-)

diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -898,6 +898,10 @@ "name": "Hidden Snapchat Plus Features", "description": "Enables unreleased/beta Snapchat Plus features\nMight not work on older Snapchat versions" }, + "custom_streaks_expiration_format": { + "name": "Custom Streaks Expiration Format", + "description": "Customizes the Streaks Expiration format\n\nAvailable variables:\n - %c: Streaks Count\n - %e: Hourglass Emoji\n - %d: Days\n - %h: Hours\n - %m: Minutes\n - %s: Seconds\n - %w: Remaining Time" + }, "disable_composer_modules": { "name": "Disable Composer Modules", "description": "Prevents selected composer modules from being loaded\nNames must be separated by a comma" diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt @@ -45,6 +45,7 @@ class Experimental : ConfigContainer() { addNotices(FeatureNotice.BAN_RISK, FeatureNotice.UNSTABLE) requireRestart() } + val customStreaksExpirationFormat = string("custom_streaks_expiration_format") { requireRestart() } val addFriendSourceSpoof = unique("add_friend_source_spoof", "added_by_username", "added_by_mention", diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt @@ -129,6 +129,7 @@ class FeatureManager( MediaFilePicker(), HideActiveMusic(), AutoOpenSnaps(), + CustomStreaksExpirationFormat(), ) initializeFeatures() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomStreaksExpirationFormat.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomStreaksExpirationFormat.kt @@ -0,0 +1,43 @@ +package me.rhunk.snapenhance.core.features.impl.ui + +import me.rhunk.snapenhance.core.features.Feature +import me.rhunk.snapenhance.core.features.FeatureLoadParams +import me.rhunk.snapenhance.core.util.hook.HookStage +import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.mapper.impl.StreaksExpirationMapper +import kotlin.time.Duration.Companion.milliseconds + +class CustomStreaksExpirationFormat: Feature("CustomStreaksExpirationFormat", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + private fun Long.padZero(): String { + return this.toString().padStart(2, '0') + } + + override fun onActivityCreate() { + val expirationFormat by context.config.experimental.customStreaksExpirationFormat + if (expirationFormat.isEmpty()) return + + context.mappings.useMapper(StreaksExpirationMapper::class) { + findClass(streaksFormatterClass.get() ?: return@useMapper).hook(formatStreaksTextMethod.get() ?: return@useMapper, HookStage.AFTER) { param -> + val streaksCount = param.argNullable(2) ?: 0 + val streaksExpiration = param.argNullable<Any>(3) ?: return@hook + + val hourGlassTimeRemaining = streaksExpiration.getObjectField(hourGlassTimeRemainingField.get() ?: return@hook) as? Long ?: return@hook + val expirationTime = streaksExpiration.getObjectField(expirationTimeField.get() ?: return@hook) as? Long ?: return@hook + val delta = (expirationTime - System.currentTimeMillis()).milliseconds + + val hourGlassEmoji = if (delta.inWholeMilliseconds in 1..hourGlassTimeRemaining) if (expirationTime % 2 == 0L) "⏳" else "⌛" else "" + + param.setResult(expirationFormat + .replace("%c", streaksCount.toString()) + .replace("%e", hourGlassEmoji) + .replace("%d", delta.inWholeDays.toString()) + .replace("%h", (delta.inWholeHours % 24).padZero()) + .replace("%m", (delta.inWholeMinutes % 60).padZero()) + .replace("%s", (delta.inWholeSeconds % 60).padZero()) + .replace("%w", delta.toString()) + ) + } + } + } +}+ \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt @@ -35,6 +35,7 @@ class ClassMapper( FriendingDataSourcesMapper(), OperaViewerParamsMapper(), MemoriesPresenterMapper(), + StreaksExpirationMapper(), ) } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StreaksExpirationMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StreaksExpirationMapper.kt @@ -0,0 +1,47 @@ +package me.rhunk.snapenhance.mapper.impl + +import me.rhunk.snapenhance.mapper.AbstractClassMapper +import me.rhunk.snapenhance.mapper.ext.findConstString +import me.rhunk.snapenhance.mapper.ext.getClassName +import me.rhunk.snapenhance.mapper.ext.searchNextFieldReference +import java.lang.reflect.Modifier + +class StreaksExpirationMapper: AbstractClassMapper("StreaksExpirationMapper") { + val hourGlassTimeRemainingField = string("hourGlassTimeRemainingField") + val expirationTimeField = string("expirationTimeField") + + val streaksFormatterClass = string("streaksFormatterClass") + val formatStreaksTextMethod = string("formatStreaksTextMethod") + + init { + mapper { + var streaksExpirationClassName: String? = null + for (clazz in classes) { + val toStringMethod = clazz.methods.firstOrNull { it.name == "toString" } ?: continue + if (toStringMethod.implementation?.findConstString("StreaksExpiration(", contains = true) != true) continue + + streaksExpirationClassName = clazz.getClassName() + toStringMethod.implementation?.apply { + hourGlassTimeRemainingField.set(searchNextFieldReference("hourGlassTimeRemaining", contains = true)?.name) + expirationTimeField.set(searchNextFieldReference("expirationTime", contains = true)?.name) + } + break + } + + if (streaksExpirationClassName == null) return@mapper + + for (clazz in classes) { + val formatStreaksTextDexMethod = clazz.methods.firstOrNull { method -> + Modifier.isStatic(method.accessFlags) && + method.returnType == "Ljava/lang/String;" && + method.parameterTypes.let { + it.size >= 4 && it[0] == "Ljava/util/Map;" && it[2] == "Ljava/lang/Integer;" && it[3].contains(streaksExpirationClassName) + } + } ?: continue + streaksFormatterClass.set(clazz.getClassName()) + formatStreaksTextMethod.set(formatStreaksTextDexMethod.name) + break + } + } + } +}+ \ No newline at end of file