From dbbc52b67f65ec003c0f5b308b639928225cae2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=9ET=CE=9ERNAL?= Date: Wed, 25 Mar 2026 21:59:31 +0530 Subject: [PATCH] v1.5.3 --- .../pages/features/FeaturesRootSection.kt | 4 +- .../purrfectsnap/ui/util/AlertDialogs.kt | 6 +- .../ui/util/AndroidDialogCustom.kt | 152 +++++++++++++++++- build.gradle.kts | 4 +- changelogs-stable.txt | 6 + common/src/main/assets/lang/en_UK.json | 10 +- common/src/main/assets/lang/en_US.json | 22 ++- .../common/config/impl/MessagingTweaks.kt | 6 + .../features/impl/messaging/AutoMarkAsRead.kt | 133 +++++++++++++-- .../features/impl/ui/MessageIndicators.kt | 2 +- .../core/features/impl/ui/SnapPreview.kt | 16 +- gradle.properties | 5 +- 12 files changed, 331 insertions(+), 35 deletions(-) diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt index aed906b4..2b0aadf0 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt @@ -53,8 +53,6 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.zIndex import androidx.core.net.toUri import androidx.lifecycle.Lifecycle @@ -78,6 +76,8 @@ import me.eternal.purrfectsnap.ui.manager.Routes import me.eternal.purrfectsnap.ui.manager.ManagerTheme import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette import me.eternal.purrfectsnap.ui.util.* +import me.eternal.purrfectsnap.ui.util.Dialog +import me.eternal.purrfectsnap.ui.util.DialogProperties import org.json.JSONArray import org.json.JSONObject import kotlin.math.max diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/AlertDialogs.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/AlertDialogs.kt index 8ebc7f80..50b7e5bc 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/AlertDialogs.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/AlertDialogs.kt @@ -40,7 +40,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import me.eternal.purrfectsnap.common.config.ConfigFlag import androidx.compose.ui.viewinterop.AndroidView -import androidx.compose.ui.window.Dialog as StandardDialog import androidx.core.net.toUri import com.github.skydoves.colorpicker.compose.* import com.google.gson.JsonParser @@ -67,6 +66,7 @@ import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Overlay import java.io.File import me.eternal.purrfectsnap.ui.util.purrfectSwitchColors +import me.eternal.purrfectsnap.ui.util.Dialog as StandardDialog class AlertDialogs( @@ -1093,7 +1093,7 @@ class AlertDialogs( val lat = remember { mutableStateOf(coordinates.first.toString()) } val lon = remember { mutableStateOf(coordinates.second.toString()) } - Dialog( + StandardDialog( onDismissRequest = { customCoordinatesDialog = false }, @@ -1398,7 +1398,7 @@ class AlertDialogs( // Add/Edit message dialog if (showAddDialog) { - Dialog( + StandardDialog( onDismissRequest = { showAddDialog = false }, properties = DialogProperties( usePlatformDefaultWidth = false diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/AndroidDialogCustom.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/AndroidDialogCustom.kt index 4e267a7e..7297e26c 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/AndroidDialogCustom.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/AndroidDialogCustom.kt @@ -4,12 +4,28 @@ package me.eternal.purrfectsnap.ui.util import android.annotation.SuppressLint import android.app.Activity import android.content.Context +import android.content.ContextWrapper import android.graphics.Outline import android.os.Build import android.provider.Settings import android.view.* +import android.view.View.OnAttachStateChangeListener import androidx.activity.ComponentDialog import androidx.activity.addCallback +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.ExperimentalComposeUiApi @@ -19,9 +35,12 @@ import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.* import androidx.compose.ui.semantics.dialog import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.SecureFlagPolicy import androidx.core.view.WindowCompat import androidx.lifecycle.findViewTreeLifecycleOwner @@ -33,6 +52,12 @@ import androidx.savedstate.setViewTreeSavedStateRegistryOwner import java.util.UUID import kotlin.math.roundToInt +private tailrec fun Context.findActivity(): Activity? = when (this) { + is Activity -> this + is ContextWrapper -> baseContext?.findActivity() + else -> null +} + class DialogProperties constructor( val dismissOnBackPress: Boolean = true, val dismissOnClickOutside: Boolean = true, @@ -83,6 +108,17 @@ fun Dialog( content: @Composable () -> Unit ) { val view = LocalView.current + var forceInline by remember(view) { mutableStateOf(false) } + val hostActivity = remember(view) { view.context.findActivity() } + val shouldUseInline = forceInline || hostActivity == null || hostActivity.isFinishing || hostActivity.isDestroyed + if (shouldUseInline) { + InlineDialog( + onDismissRequest = onDismissRequest, + dismissOnClickOutside = properties.dismissOnClickOutside, + content = content + ) + return + } val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current val composition = rememberCompositionContext() @@ -110,11 +146,30 @@ fun Dialog( } DisposableEffect(dialog) { - // Set the dialog's window type to TYPE_APPLICATION_OVERLAY so it's compatible with compose overlays - if (Settings.canDrawOverlays(view.context) && view.context !is Activity) { - dialog.window?.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY) + val showDialog = { + try { + dialog.prepareWindowForHost() + if (!dialog.isShowing) { + dialog.show() + } + } catch (_: WindowManager.BadTokenException) { + forceInline = true + } + } + + if (view.isAttachedToWindow || view.windowToken != null || view.rootView?.windowToken != null) { + showDialog() + } else { + val listener = object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + v.removeOnAttachStateChangeListener(this) + showDialog() + } + + override fun onViewDetachedFromWindow(v: View) = Unit + } + view.addOnAttachStateChangeListener(listener) } - dialog.show() onDispose { dialog.dismiss() @@ -131,6 +186,75 @@ fun Dialog( } } +@Composable +private fun InlineDialog( + onDismissRequest: () -> Unit, + dismissOnClickOutside: Boolean, + content: @Composable () -> Unit +) { + val density = LocalDensity.current + val displayMetrics = LocalContext.current.resources.displayMetrics + val screenWidthDp = with(density) { displayMetrics.widthPixels.toDp() } + val screenHeightDp = with(density) { displayMetrics.heightPixels.toDp() } + val interactionSource = remember { MutableInteractionSource() } + var visible by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + visible = true + } + + Popup( + alignment = androidx.compose.ui.Alignment.Center, + properties = PopupProperties( + focusable = true, + dismissOnBackPress = true, + dismissOnClickOutside = dismissOnClickOutside + ), + onDismissRequest = onDismissRequest + ) { + Box( + modifier = Modifier + .width(screenWidthDp) + .height(screenHeightDp) + .then( + if (dismissOnClickOutside) { + Modifier.clickable( + interactionSource = interactionSource, + indication = null, + onClick = onDismissRequest + ) + } else { + Modifier + } + ) + .semantics { dialog() }, + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + AnimatedVisibility( + visible = visible, + enter = fadeIn(animationSpec = tween(180)) + scaleIn( + initialScale = 0.92f, + animationSpec = spring(dampingRatio = 0.82f, stiffness = 520f) + ), + exit = fadeOut(animationSpec = tween(120)) + scaleOut( + targetScale = 0.96f, + animationSpec = tween(120) + ) + ) { + Box( + modifier = Modifier.clickable( + interactionSource = interactionSource, + indication = null, + onClick = {} + ) + ) { + content() + } + } + } + } +} + interface DialogWindowProvider { val window: Window } @@ -279,6 +403,26 @@ private class DialogWrapper( } } + fun prepareWindowForHost() { + val hostActivity = composeView.context.findActivity() + val hostToken = composeView.applicationWindowToken + ?: composeView.windowToken + ?: composeView.rootView?.applicationWindowToken + ?: composeView.rootView?.windowToken + if (hostActivity == null) { + when { + hostToken != null -> window?.setType(WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG) + Settings.canDrawOverlays(composeView.context) -> window?.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY) + } + } + if (hostToken != null) { + window?.attributes = window?.attributes?.apply { + token = hostToken + } + } + hostActivity?.let { setOwnerActivity(it) } + } + private fun setLayoutDirection(layoutDirection: LayoutDirection) { dialogLayout.layoutDirection = when (layoutDirection) { LayoutDirection.Ltr -> android.util.LayoutDirection.LTR diff --git a/build.gradle.kts b/build.gradle.kts index 972b6f64..d245d176 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,8 +33,8 @@ tasks.register("getVersion") { } // You can still set these for legacy use by submodules or scripts: -rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.5.2").get()) -rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("296").get().toInt()) +rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.5.3").get()) +rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("298").get().toInt()) rootProject.ext.set("applicationId", "me.eternal.purrfectsnap") // buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate. // Include version code so each release has a different hash; use random for uniqueness within same version. diff --git a/changelogs-stable.txt b/changelogs-stable.txt index 80f38b4c..fe1b0574 100644 --- a/changelogs-stable.txt +++ b/changelogs-stable.txt @@ -1,3 +1,9 @@ +## v1.5.3 +- New: Mark as Seen Mode(Limit per run[Custom] or Complete Queue) +- Fix: PurrfectSnap crash if you try to open any dialog setting through in-app overlay +- New: Translucent Dialogs & refreshed animation +- Fix: Adjusted Snap Preview Location + ## v1.5.2 - New: Continuous snap sender feature! - Fix: Snap send failure for E2E Chats diff --git a/common/src/main/assets/lang/en_UK.json b/common/src/main/assets/lang/en_UK.json index a347e8ac..0567f492 100644 --- a/common/src/main/assets/lang/en_UK.json +++ b/common/src/main/assets/lang/en_UK.json @@ -1190,6 +1190,14 @@ "name": "Mark Snap as Seen Button", "description": "Adds a button to mark a Snap as seen when viewing it.\nThis will work even when Stealth Mode is enabled" }, + "mark_snap_as_seen_processing_mode": { + "name": "Mark Snaps as Seen Mode", + "description": "Choose whether to process a limited number of snaps per run or the entire queue at once" + }, + "mark_snap_as_seen_limit": { + "name": "Mark Snaps as Seen Limit", + "description": "How many snaps to process per run when the mode is set to limit" + }, "skip_when_marking_as_seen": { "name": "Skip When Marking as Seen", "description": "Automatically skips to the next Snap when marking a Snap as seen.\nUse in combination with Mark Snap as Seen Button" @@ -3639,4 +3647,4 @@ "openai": "OpenAI", "openrouter": "OpenRouter" } -} \ No newline at end of file +} diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 2aad7e80..0f68f8ae 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -1254,6 +1254,14 @@ "name": "Mark Snap as Seen Button", "description": "Adds a button to mark a Snap as seen when viewing it.\nThis will work even when Stealth Mode is enabled" }, + "mark_snap_as_seen_processing_mode": { + "name": "Mark Snaps as Seen Mode", + "description": "Choose whether to process a limited number of snaps per run or the entire queue at once" + }, + "mark_snap_as_seen_limit": { + "name": "Mark Snaps as Seen Limit", + "description": "How many snaps to process per run when the mode is set to limit" + }, "skip_when_marking_as_seen": { "name": "Skip When Marking as Seen", "description": "Automatically skips to the next Snap when marking a Snap as seen.\nUse in combination with Mark Snap as Seen Button" @@ -2514,6 +2522,10 @@ "remove_audio_note_duration": "Remove Audio Note Duration", "remove_audio_note_transcript_capability": "Remove Audio Note Transcript Capability" }, + "mark_snap_as_seen_processing_mode": { + "limit": "Limit Per Run", + "complete": "Complete Queue" + }, "hide_ui_components": { "hide_profile_call_buttons": "Remove Profile Call Buttons", "hide_chat_call_buttons": "Remove Chat Call Buttons", @@ -3437,10 +3449,10 @@ "search": { "placeholder": "Search" }, - "filters": { - "newest_first": "Newest first", - "pick_a_date": "Pick a date", - "title": "Filters", + "filters": { + "newest_first": "Newest first", + "pick_a_date": "Pick a date", + "title": "Filters", "search_by": "Search by", "since": "Since", "until": "Until", @@ -3455,7 +3467,7 @@ "started_typing": "Started typing", "stopped_typing": "Stopped typing", "started_speaking": "Started speaking", - "stopped_speaking": "Stopped speaking", + "stopped_speaking": "Stopped speaking", "started_peeking": "Started peeking", "stopped_peeking": "Stopped peeking", "message_read": "Read message", diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt index e8f959ec..60f61183 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt @@ -205,6 +205,12 @@ class MessagingTweaks : ConfigContainer() { val unlimitedSnapViewTime = boolean("unlimited_snap_view_time") val autoMarkAsRead = multiple("auto_mark_as_read", "snap_reply", "conversation_read", "save_snap_in_chat") { requireRestart() } val markSnapAsSeenButton = boolean("mark_snap_as_seen_button") { requireRestart() } + val markSnapAsSeenProcessingMode = unique("mark_snap_as_seen_processing_mode", "limit", "complete").apply { + set("limit") + } + val markSnapAsSeenLimit = integer("mark_snap_as_seen_limit", defaultValue = 50) { + inputCheck = { it.toIntOrNull()?.coerceAtLeast(1) != null } + } val skipWhenMarkingAsSeen = boolean("skip_when_marking_as_seen") { requireRestart() } val loopMediaPlayback = boolean("loop_media_playback") { requireRestart() } val disableReplayInFF = boolean("disable_replay_in_ff") diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/AutoMarkAsRead.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/AutoMarkAsRead.kt index b853e8e7..b54fa38c 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/AutoMarkAsRead.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/AutoMarkAsRead.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.WarningAmber +import androidx.compose.material.icons.filled.Info import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.mutableIntStateOf @@ -39,6 +40,54 @@ import kotlin.random.Random class AutoMarkAsRead : Feature("Auto Mark As Read") { val canMarkConversationAsRead by lazy { context.config.messaging.autoMarkAsRead.get().contains("conversation_read") } + private val markAsSeenBatchSize = 50 + private val markAsSeenBatchCooldownMs = 4000L + + private data class PendingSnapMessage( + val clientMessageId: Long, + val creationTimestamp: Long + ) + + private fun String?.isRateLimited(): Boolean { + val value = this ?: return false + return value.contains("RESOURCE_EXHAUSTED", ignoreCase = true) || + value.contains("Rate limited", ignoreCase = true) + } + + private fun showRateLimitedDialog(processed: Int, total: Int) { + val activity = context.mainActivity ?: run { + context.inAppOverlay.showStatusToast( + Icons.Default.WarningAmber, + "Rate limited after $processed/$total snaps. Try again later." + ) + return + } + + createComposeAlertDialog(activity) { + PurrfectOverlayTheme { + PurrfectGlassCard( + modifier = androidx.compose.ui.Modifier.fillMaxWidth(), + title = "Rate Limited", + subtitle = "Snapchat stopped the mark-as-seen run to protect your account", + icon = Icons.Default.WarningAmber + ) { + Column( + modifier = androidx.compose.ui.Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Processed $processed of $total snaps before the request was rate limited.", + color = PurrfectOverlayPalette.textSecondary + ) + Text( + text = "No bypass was attempted. Wait a bit and run it again, or lower the per-run limit in settings.", + color = PurrfectOverlayPalette.textSecondary + ) + } + } + } + }.show() + } fun markConversationsAsRead(conversationIds: List) { conversationIds.forEach { conversationId -> @@ -63,18 +112,66 @@ class AutoMarkAsRead : Feature("Auto Mark As Read") { } } + private fun getPendingSnapMessageIds(conversationId: String, requestedLimit: Int?): List { + val collected = mutableListOf() + val pageSize = 200 + var page = 0 + + while (true) { + val messages = context.database.getMessagesFromConversationId(conversationId, pageSize, page) ?: break + messages.forEach { message -> + if (message.contentType != ContentType.SNAP.id && message.contentType != ContentType.EXTERNAL_MEDIA.id) return@forEach + if (message.isViewedByUser == 1 || message.readTimestamp > 0L) return@forEach + collected += PendingSnapMessage( + clientMessageId = message.clientMessageId.toLong(), + creationTimestamp = message.creationTimestamp + ) + } + if (messages.size < pageSize) break + if (requestedLimit != null && collected.size >= requestedLimit) break + page++ + } + + return collected + .distinctBy { it.clientMessageId } + .sortedBy { it.creationTimestamp } + .let { pending -> + if (requestedLimit != null) pending.take(requestedLimit) else pending + } + .map { it.clientMessageId } + } + fun markSnapsAsSeen(conversationId: String) { val messaging = context.feature(Messaging::class) - val messageIds = messaging.getFeedCachedMessageIds(conversationId)?.takeIf { it.isNotEmpty() } ?: run { - context.inAppOverlay.showStatusToast( - Icons.Default.WarningAmber, - context.translation["mark_as_seen.no_unseen_snaps_toast"] - ) - return + val processingMode = context.config.messaging.markSnapAsSeenProcessingMode.get() + val configuredLimit = context.config.messaging.markSnapAsSeenLimit.get() + .coerceAtLeast(1) + val requestedLimit = if (processingMode == "complete") null else configuredLimit + val messageIds = getPendingSnapMessageIds(conversationId, requestedLimit) + .ifEmpty { + messaging.getFeedCachedMessageIds(conversationId) + ?.map { it.toLong() } + ?.takeIf { it.isNotEmpty() } + ?.let { cached -> + if (requestedLimit != null) cached.take(requestedLimit) else cached + } + ?: run { + context.inAppOverlay.showStatusToast( + Icons.Default.WarningAmber, + context.translation["mark_as_seen.no_unseen_snaps_toast"] + ) + return + } + } + val targetMessageIds = if (requestedLimit == null) { + messageIds + } else { + messageIds.take(requestedLimit) } var job: Job? = null val processedCount = mutableIntStateOf(0) + var rateLimitedAt: Int? = null val dialog = createComposeAlertDialog(context.mainActivity!!, builder = { setOnDismissListener { job?.cancel() } }) { @@ -95,7 +192,7 @@ class AutoMarkAsRead : Feature("Auto Mark As Read") { trackColor = PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.18f) ) Text( - text = "${processedCount.intValue}/${messageIds.size}", + text = "${processedCount.intValue}/${targetMessageIds.size}", color = PurrfectOverlayPalette.textSecondary ) } @@ -104,16 +201,34 @@ class AutoMarkAsRead : Feature("Auto Mark As Read") { }.apply { show() } context.coroutineScope.launch(Dispatchers.IO) { - messageIds.forEachIndexed { index, messageId -> - markSnapAsSeen(conversationId, messageId) + targetMessageIds.forEachIndexed { index, messageId -> + val result = markSnapAsSeen(conversationId, messageId) + if (result.isRateLimited()) { + rateLimitedAt = processedCount.intValue + return@launch + } delay(Random.nextLong(20, 60)) context.runOnUiThread { processedCount.intValue = index + 1 } + val processed = index + 1 + if (processed < targetMessageIds.size && processed % markAsSeenBatchSize == 0) { + delay(markAsSeenBatchCooldownMs) + } } }.also { job = it }.invokeOnCompletion { context.runOnUiThread { dialog.dismiss() + if (rateLimitedAt != null) { + val processedIndex = rateLimitedAt!! + processedCount.intValue = processedIndex + showRateLimitedDialog(processedIndex, targetMessageIds.size) + } else if (requestedLimit != null && targetMessageIds.size < messageIds.size) { + context.inAppOverlay.showStatusToast( + Icons.Default.Info, + "Processed ${targetMessageIds.size} of ${messageIds.size} unseen snaps." + ) + } } } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/MessageIndicators.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/MessageIndicators.kt index 4c820012..380ab81f 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/MessageIndicators.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/MessageIndicators.kt @@ -137,4 +137,4 @@ class MessageIndicators : Feature("Message Indicators") { } } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/SnapPreview.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/SnapPreview.kt index f833d234..10b607a9 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/SnapPreview.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/SnapPreview.kt @@ -28,6 +28,8 @@ class SnapPreview : Feature("SnapPreview") { private val bitmapCache = EvictingMap(50) // filePath => bitmap private val fetchJobTab = randomTag() + private val previewHorizontalAdjustmentDp = 16 + private val previewVerticalAdjustmentDp = 10 override fun init() { if (!context.config.userInterface.snapPreview.get()) return @@ -47,9 +49,11 @@ class SnapPreview : Feature("SnapPreview") { } onNextActivityCreate { - val (chatMediaCardHeight, chatMediaCardSnapMargin, chatMediaCardSnapMarginStartSdl) = context.userInterface.run { - Triple(dpToPx(60), dpToPx(10), dpToPx(15)) - } + val chatMediaCardHeight = context.userInterface.dpToPx(60) + val chatMediaCardSnapMargin = context.userInterface.dpToPx(10) + val chatMediaCardSnapMarginStartSdl = context.userInterface.dpToPx(15) + val previewHorizontalAdjustment = context.userInterface.dpToPx(previewHorizontalAdjustmentDp) + val previewVerticalAdjustment = context.userInterface.dpToPx(previewVerticalAdjustmentDp) fun decodeMedia(file: File) = runCatching { bitmapCache.getOrPut(file.absolutePath) { @@ -91,8 +95,8 @@ class SnapPreview : Feature("SnapPreview") { val bitmap = bitmapCache[mediaFilePath] ?: return canvas.drawBitmap(bitmap, - canvas.width.toFloat() - bitmap.width - chatMediaCardSnapMarginStartSdl.toFloat() - chatMediaCardSnapMargin.toFloat(), - (canvas.height - bitmap.height) / 2f, + canvas.width.toFloat() - bitmap.width - chatMediaCardSnapMarginStartSdl.toFloat() - chatMediaCardSnapMargin.toFloat() + previewHorizontalAdjustment, + (canvas.height - bitmap.height) / 2f + previewVerticalAdjustment, null ) } @@ -101,4 +105,4 @@ class SnapPreview : Feature("SnapPreview") { } } } -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index b10bcf00..95135ae0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,10 +7,11 @@ org.gradle.configuration-cache=true org.gradle.configuration-cache.problems=warn nativeAbis=arm64-v8a -APP_VERSION_NAME=1.5.2 -APP_VERSION_CODE=296 +APP_VERSION_NAME=1.5.3 +APP_VERSION_CODE=298 debug_build_hash=18fe2a814d0e2eb5 psIntegrityPinnedSha256= EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c android.disallowKotlinSourceSets=false android.sourceset.disallowProvider=false +ksp.incremental=false