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:
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