mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-13 05:37:48 +02:00
feat: in-chat snap preview
This commit is contained in:
@ -232,6 +232,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"snap_preview": {
|
||||||
|
"name": "Snap Preview",
|
||||||
|
"description": "Displays a small preview next to unseen Snaps in chat"
|
||||||
|
},
|
||||||
"bootstrap_override": {
|
"bootstrap_override": {
|
||||||
"name": "Bootstrap Override",
|
"name": "Bootstrap Override",
|
||||||
"description": "Overrides user interface bootstrap settings",
|
"description": "Overrides user interface bootstrap settings",
|
||||||
|
@ -26,6 +26,7 @@ class UserInterfaceTweaks : ConfigContainer() {
|
|||||||
val friendFeedMenuPosition = integer("friend_feed_menu_position", defaultValue = 1)
|
val friendFeedMenuPosition = integer("friend_feed_menu_position", defaultValue = 1)
|
||||||
val amoledDarkMode = boolean("amoled_dark_mode") { addNotices(FeatureNotice.UNSTABLE); requireRestart() }
|
val amoledDarkMode = boolean("amoled_dark_mode") { addNotices(FeatureNotice.UNSTABLE); requireRestart() }
|
||||||
val friendFeedMessagePreview = container("friend_feed_message_preview", FriendFeedMessagePreview()) { requireRestart() }
|
val friendFeedMessagePreview = container("friend_feed_message_preview", FriendFeedMessagePreview()) { requireRestart() }
|
||||||
|
val snapPreview = boolean("snap_preview") { addNotices(FeatureNotice.UNSTABLE); requireRestart() }
|
||||||
val bootstrapOverride = container("bootstrap_override", BootstrapOverride()) { requireRestart() }
|
val bootstrapOverride = container("bootstrap_override", BootstrapOverride()) { requireRestart() }
|
||||||
val mapFriendNameTags = boolean("map_friend_nametags") { requireRestart() }
|
val mapFriendNameTags = boolean("map_friend_nametags") { requireRestart() }
|
||||||
val streakExpirationInfo = boolean("streak_expiration_info") { requireRestart() }
|
val streakExpirationInfo = boolean("streak_expiration_info") { requireRestart() }
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
package me.rhunk.snapenhance.core.features.impl.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.*
|
||||||
|
import android.graphics.drawable.ShapeDrawable
|
||||||
|
import android.graphics.drawable.shapes.Shape
|
||||||
|
import me.rhunk.snapenhance.common.Constants
|
||||||
|
import me.rhunk.snapenhance.common.data.ContentType
|
||||||
|
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||||
|
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
||||||
|
import me.rhunk.snapenhance.core.features.Feature
|
||||||
|
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||||
|
import me.rhunk.snapenhance.core.ui.addForegroundDrawable
|
||||||
|
import me.rhunk.snapenhance.core.ui.removeForegroundDrawable
|
||||||
|
import me.rhunk.snapenhance.core.util.EvictingMap
|
||||||
|
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.core.util.media.PreviewUtils
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||||
|
private val mediaFileCache = mutableMapOf<String, File>() // mMediaId => mediaFile
|
||||||
|
private val bitmapCache = EvictingMap<String, Bitmap>(50) // filePath => bitmap
|
||||||
|
|
||||||
|
private val isEnabled get() = context.config.userInterface.snapPreview.get()
|
||||||
|
|
||||||
|
override fun init() {
|
||||||
|
if (!isEnabled) return
|
||||||
|
context.mappings.getMappedClass("callbacks", "ContentCallback").hook("handleContentResult", HookStage.BEFORE) { param ->
|
||||||
|
val contentResult = param.arg<Any>(0)
|
||||||
|
val classMethods = contentResult::class.java.methods
|
||||||
|
|
||||||
|
val contentKey = classMethods.find { it.name == "getContentKey" }?.invoke(contentResult) ?: return@hook
|
||||||
|
if (contentKey.getObjectField("mMediaContextType").toString() != "CHAT") return@hook
|
||||||
|
|
||||||
|
val filePath = classMethods.find { it.name == "getFilePath" }?.invoke(contentResult) ?: return@hook
|
||||||
|
val mediaId = contentKey.getObjectField("mMediaId").toString()
|
||||||
|
|
||||||
|
mediaFileCache[mediaId.substringAfter("-")] = File(filePath.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DiscouragedApi")
|
||||||
|
override fun onActivityCreate() {
|
||||||
|
if (!isEnabled) return
|
||||||
|
val chatMediaCardHeight = context.resources.getDimensionPixelSize(context.resources.getIdentifier("chat_media_card_height", "dimen", Constants.SNAPCHAT_PACKAGE_NAME))
|
||||||
|
val chatMediaCardSnapMargin = context.resources.getDimensionPixelSize(context.resources.getIdentifier("chat_media_card_snap_margin", "dimen", Constants.SNAPCHAT_PACKAGE_NAME))
|
||||||
|
val chatMediaCardSnapMarginStartSdl = context.resources.getDimensionPixelSize(context.resources.getIdentifier("chat_media_card_snap_margin_start_sdl", "dimen", Constants.SNAPCHAT_PACKAGE_NAME))
|
||||||
|
|
||||||
|
fun decodeMedia(file: File) = runCatching {
|
||||||
|
bitmapCache.getOrPut(file.absolutePath) {
|
||||||
|
PreviewUtils.resizeBitmap(
|
||||||
|
PreviewUtils.createPreviewFromFile(file) ?: return@runCatching null,
|
||||||
|
chatMediaCardHeight - chatMediaCardSnapMargin,
|
||||||
|
chatMediaCardHeight - chatMediaCardSnapMargin
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
context.event.subscribe(BindViewEvent::class) { event ->
|
||||||
|
event.chatMessage { _, messageId ->
|
||||||
|
event.view.removeForegroundDrawable("snapPreview")
|
||||||
|
|
||||||
|
val message = context.database.getConversationMessageFromId(messageId.toLong()) ?: return@chatMessage
|
||||||
|
val messageReader = ProtoReader(message.messageContent ?: return@chatMessage)
|
||||||
|
val contentType = ContentType.fromMessageContainer(messageReader.followPath(4, 4))
|
||||||
|
|
||||||
|
if (contentType != ContentType.SNAP) return@chatMessage
|
||||||
|
|
||||||
|
val mediaIdKey = messageReader.getString(4, 5, 1, 3, 2, 2) ?: return@chatMessage
|
||||||
|
|
||||||
|
event.view.addForegroundDrawable("snapPreview", ShapeDrawable(object: Shape() {
|
||||||
|
override fun draw(canvas: Canvas, paint: Paint) {
|
||||||
|
if (canvas.height / context.resources.displayMetrics.density > 90) return
|
||||||
|
val bitmap = mediaFileCache[mediaIdKey]?.let { decodeMedia(it) } ?: return
|
||||||
|
|
||||||
|
canvas.drawBitmap(bitmap,
|
||||||
|
canvas.width.toFloat() - bitmap.width - chatMediaCardSnapMarginStartSdl.toFloat() - chatMediaCardSnapMargin.toFloat(),
|
||||||
|
(canvas.height - bitmap.height) / 2f,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -102,6 +102,7 @@ class FeatureManager(
|
|||||||
HideFriendFeedEntry::class,
|
HideFriendFeedEntry::class,
|
||||||
HideQuickAddFriendFeed::class,
|
HideQuickAddFriendFeed::class,
|
||||||
CallStartConfirmation::class,
|
CallStartConfirmation::class,
|
||||||
|
SnapPreview::class,
|
||||||
)
|
)
|
||||||
|
|
||||||
initializeFeatures()
|
initializeFeatures()
|
||||||
|
@ -8,6 +8,7 @@ import android.media.MediaDataSource
|
|||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import me.rhunk.snapenhance.common.data.FileType
|
import me.rhunk.snapenhance.common.data.FileType
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
object PreviewUtils {
|
object PreviewUtils {
|
||||||
fun createPreview(data: ByteArray, isVideo: Boolean): Bitmap? {
|
fun createPreview(data: ByteArray, isVideo: Boolean): Bitmap? {
|
||||||
@ -52,14 +53,20 @@ object PreviewUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resizeBitmap(bitmap: Bitmap, outWidth: Int, outHeight: Int): Bitmap? {
|
fun resizeBitmap(source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
|
||||||
val scaleWidth = outWidth.toFloat() / bitmap.width
|
val sourceWidth = source.getWidth()
|
||||||
val scaleHeight = outHeight.toFloat() / bitmap.height
|
val sourceHeight = source.getHeight()
|
||||||
val matrix = Matrix()
|
val scale = max(outWidth.toFloat() / sourceWidth, outHeight.toFloat() / sourceHeight)
|
||||||
matrix.postScale(scaleWidth, scaleHeight)
|
|
||||||
val resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)
|
val dx = (outWidth - (scale * sourceWidth)) / 2F
|
||||||
bitmap.recycle()
|
val dy = (outHeight - (scale * sourceHeight)) / 2F
|
||||||
return resizedBitmap
|
val dest = Bitmap.createBitmap(outWidth, outHeight, source.getConfig())
|
||||||
|
val canvas = Canvas(dest)
|
||||||
|
canvas.drawBitmap(source, Matrix().apply {
|
||||||
|
postScale(scale, scale)
|
||||||
|
postTranslate(dx, dy)
|
||||||
|
}, null)
|
||||||
|
return dest
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mergeBitmapOverlay(originalMedia: Bitmap, overlayLayer: Bitmap): Bitmap {
|
fun mergeBitmapOverlay(originalMedia: Bitmap, overlayLayer: Bitmap): Bitmap {
|
||||||
|
@ -17,6 +17,10 @@ class CallbackMapper : AbstractClassMapper() {
|
|||||||
|
|
||||||
if (clazz.getClassName().endsWith("\$CppProxy")) return@filter false
|
if (clazz.getClassName().endsWith("\$CppProxy")) return@filter false
|
||||||
|
|
||||||
|
// ignore dummy ContentCallback class
|
||||||
|
if (superclassName.endsWith("ContentCallback") && !clazz.methods.first { it.name == "<init>" }.parameterTypes.contains("Z"))
|
||||||
|
return@filter false
|
||||||
|
|
||||||
val superClass = getClass(clazz.superclass) ?: return@filter false
|
val superClass = getClass(clazz.superclass) ?: return@filter false
|
||||||
!superClass.isFinal()
|
!superClass.isFinal()
|
||||||
}.map {
|
}.map {
|
||||||
|
Reference in New Issue
Block a user