feat(experimental): custom streaks expiration format
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -129,6 +129,7 @@ class FeatureManager(
|
||||
MediaFilePicker(),
|
||||
HideActiveMusic(),
|
||||
AutoOpenSnaps(),
|
||||
CustomStreaksExpirationFormat(),
|
||||
)
|
||||
|
||||
initializeFeatures()
|
||||
|
||||
@@ -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())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ class ClassMapper(
|
||||
FriendingDataSourcesMapper(),
|
||||
OperaViewerParamsMapper(),
|
||||
MemoriesPresenterMapper(),
|
||||
StreaksExpirationMapper(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user