This commit is contained in:
ΞTΞRNAL
2026-03-29 12:35:34 +05:30
parent 33664c1112
commit 6491288513
8 changed files with 242 additions and 302 deletions

View File

@@ -33,8 +33,8 @@ tasks.register<GetVersionTask>("getVersion") {
}
// You can still set these for legacy use by submodules or scripts:
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.0").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("310").get().toInt())
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.1").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("312").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.

View File

@@ -1,3 +1,8 @@
## v1.6.1
- Fix: Custom Frame Rate
- Fix: Conversation Sound Style
- Fix: Auto Reactions when downloading stories(Removed story thumbnail feature)
## v1.6.0
- New: Conversation Sound Effects!(Sound when you send or receive a msg while in chat)
- New: Call Metadata Notifier!

View File

@@ -1300,10 +1300,6 @@
"name": "Call Metadata Notifier",
"description": "Shows a notification with captured call metadata after the call ends"
},
"conversation_sound_effects": {
"name": "Conversation Sound Effects",
"description": "Plays send and receive sounds inside an open conversation"
},
"conversation_sound_effects_style": {
"name": "Conversation Sound Style",
"description": "Choose the sound style used for in-conversation send and receive sounds"
@@ -2529,6 +2525,7 @@
"null": "Device default FPS"
},
"conversation_sound_effects_style": {
"disabled": "Disabled",
"imessage": "iMessage",
"telegram": "Telegram",
"whatsapp": "WhatsApp",

View File

@@ -224,12 +224,12 @@ class MessagingTweaks : ConfigContainer() {
val callStartConfirmation = boolean("call_start_confirmation") { requireRestart() }
val blockCalls = boolean("block_calls") { requireRestart() }
val callMetadataNotifier = boolean("call_metadata_notifier") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
val conversationSoundEffects = boolean("conversation_sound_effects") { requireRestart() }
val conversationSoundEffectsStyle = unique("conversation_sound_effects_style", "imessage", "telegram", "whatsapp", "subtle") {
val conversationSoundEffectsStyle = unique("conversation_sound_effects_style", "disabled", "imessage", "telegram", "whatsapp", "subtle") {
requireRestart()
customOptionTranslationPath = "conversation_sound_effects_style"
addFlags(ConfigFlag.NO_TRANSLATE)
}.apply { set("imessage") }
addFlags(ConfigFlag.NO_DISABLE_KEY)
}.apply { set("disabled") }
val unlimitedConversationPinning = boolean("unlimited_conversation_pinning") { requireRestart() }
val disableSnapModeRestrictions = boolean("disable_snap_mode_restrictions") { requireRestart() }
val autoSaveMessagesInConversations = multiple("auto_save_messages_in_conversations",

View File

@@ -12,7 +12,6 @@ import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.compose.foundation.background
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Box
@@ -30,14 +29,11 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -45,8 +41,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -125,11 +119,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
private var lastSeenMediaInfoMap: MutableMap<SplitMediaAssetType, MediaInfo>? = null
var lastSeenMapParams: ParamMap? = null
private set
private val storyPreviewCache = mutableMapOf<String, MutableMap<Int, Bitmap>>()
@Volatile
private var pendingBatchDownloadIndices: MutableList<Int>? = null
@Volatile
private var batchForceAllowDuplicate: Boolean = false
private val translations by lazy {
context.translation.getCategory("download_processor")
}
@@ -267,68 +256,10 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
val tr = context.translation.getCategory("download_processor.story_snap_dialog")
val cancelStr = context.translation["button.cancel"]
val downloadStr = context.translation["button.download"]
val previewCacheKey = buildString {
append(paramMap["STORY_ID"]?.toString() ?: "story")
append("|")
append(paramMap["TOPIC_SNAP_CREATOR_USER_ID"]?.toString() ?: "user")
append("|")
append(totalCount)
}
context.runOnUiThread {
createComposeAlertDialog(context.mainActivity!!) { alertDialog ->
PurrfectOverlayTheme {
val selected = remember { mutableStateListOf<Int>().apply { add(currentIndex) } }
val previewBitmaps = remember { mutableStateMapOf<Int, Bitmap?>() }
val previewLoading = remember { mutableStateMapOf<Int, Boolean>() }
LaunchedEffect(Unit) {
if (!selected.contains(currentIndex)) selected.add(currentIndex)
mediaInfoMap[SplitMediaAssetType.ORIGINAL]?.uri?.let { currentUri ->
previewLoading[currentIndex] = true
previewBitmaps[currentIndex] = withContext(Dispatchers.IO) { loadStoryPreviewBitmap(currentUri) }
previewLoading[currentIndex] = false
}
synchronized(storyPreviewCache) {
storyPreviewCache[previewCacheKey]?.forEach { (index, bitmap) ->
previewBitmaps[index] = bitmap
}
}
}
LaunchedEffect(previewCacheKey) {
val overlay = context.feature(OperaStoryOverlay::class)
val cachedIndices = synchronized(storyPreviewCache) {
storyPreviewCache.getOrPut(previewCacheKey) { mutableMapOf() }.keys.toSet()
}
val indicesToScan = (0 until totalCount).filter { it != currentIndex && it !in cachedIndices }
try {
for (targetIndex in indicesToScan) {
val jumped = withContext(Dispatchers.Main) {
overlay.requestJumpToSnap(targetIndex, totalCount)
}
if (!jumped) continue
val reached = waitForStoryIndex(targetIndex)
if (!reached) continue
val uri = lastSeenMediaInfoMap?.get(SplitMediaAssetType.ORIGINAL)?.uri ?: continue
previewLoading[targetIndex] = true
val bitmap = withContext(Dispatchers.IO) { loadStoryPreviewBitmap(uri) }
previewLoading[targetIndex] = false
if (bitmap != null) {
previewBitmaps[targetIndex] = bitmap
synchronized(storyPreviewCache) {
storyPreviewCache.getOrPut(previewCacheKey) { mutableMapOf() }[targetIndex] = bitmap
}
}
}
} finally {
withContext(Dispatchers.Main) {
overlay.requestJumpToSnap(currentIndex, totalCount)
}
}
}
PurrfectGlassCard(
title = tr["title"],
@@ -362,35 +293,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
},
colors = CheckboxDefaults.colors(checkedColor = PurrfectOverlayPalette.glowPrimary)
)
Box(
modifier = Modifier
.size(54.dp)
.background(Color.White.copy(alpha = 0.06f), RoundedCornerShape(12.dp)),
contentAlignment = Alignment.Center
) {
val rowBitmap = previewBitmaps[index]
val rowLoading = previewLoading[index] == true
when {
rowBitmap != null -> Image(
bitmap = rowBitmap.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.size(54.dp)
.background(Color.Transparent, RoundedCornerShape(12.dp)),
contentScale = ContentScale.Crop
)
rowLoading -> CircularProgressIndicator(
color = PurrfectOverlayPalette.glowPrimary,
modifier = Modifier.size(22.dp),
strokeWidth = 2.dp
)
else -> Icon(
imageVector = Icons.Outlined.Image,
contentDescription = null,
tint = PurrfectOverlayPalette.textSecondary
)
}
}
Text(
label,
style = MaterialTheme.typography.bodyMedium,
@@ -435,10 +337,15 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
}
Button(
onClick = {
if (selected.isNotEmpty()) {
startBatchDownload(selected.sorted().toMutableList(), allowDuplicate)
alertDialog.dismiss()
if (!selected.contains(currentIndex)) return@Button
context.executeAsync {
runCatching { handleOperaMedia(paramMap, mediaInfoMap, true, allowDuplicate) }
.onFailure {
context.log.error("Story download failed", it)
context.shortToast(translations["failed_generic_toast"])
}
}
alertDialog.dismiss()
},
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(14.dp),
@@ -457,124 +364,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
}
}
private suspend fun waitForStoryIndex(targetIndex: Int, timeoutMs: Long = 3000L): Boolean {
val startedAt = System.currentTimeMillis()
while (System.currentTimeMillis() - startedAt < timeoutMs) {
if (lastSeenMapParams?.getStorySnapIndex() == targetIndex) return true
kotlinx.coroutines.delay(60L)
}
return false
}
private fun loadStoryPreviewBitmap(uriString: String): Bitmap? {
return runCatching {
val uri = Uri.parse(uriString)
when (uri.scheme?.lowercase()) {
"content" -> context.androidContext.contentResolver.openInputStream(uri)?.use(BitmapFactory::decodeStream)
"file", null -> BitmapFactory.decodeFile(uri.path)
"http", "https" -> {
runCatching {
OkHttpClient().newCall(Request.Builder().url(uriString).build()).execute().use { response ->
response.body?.byteStream()?.use { stream -> BitmapFactory.decodeStream(stream) }
}
}.getOrNull() ?: run {
val retriever = MediaMetadataRetriever()
try {
retriever.setDataSource(uriString, emptyMap())
retriever.frameAtTime
} finally {
runCatching { retriever.release() }
}
}
}
else -> null
} ?: run {
val retriever = MediaMetadataRetriever()
try {
retriever.setDataSource(context.androidContext, uri)
retriever.frameAtTime
} finally {
runCatching { retriever.release() }
}
}
}.getOrNull()
}
private fun startBatchDownload(indices: MutableList<Int>, allowDuplicate: Boolean) {
if (indices.isEmpty()) return
val paramMap = lastSeenMapParams ?: return
val mediaInfoMap = lastSeenMediaInfoMap ?: return
pendingBatchDownloadIndices = indices
batchForceAllowDuplicate = allowDuplicate
val currentIndex = paramMap.getStorySnapIndex() ?: 0
val targetIndex = indices.first()
val totalCount = paramMap.getStorySnapTotal()
if (currentIndex == targetIndex) {
processNextBatchDownload(paramMap, mediaInfoMap)
} else {
val jumped = context.feature(OperaStoryOverlay::class).requestJumpToSnap(targetIndex, totalCount)
if (!jumped) {
pendingBatchDownloadIndices = null
context.shortToast(translations["batch_download_jump_failed_toast"])
}
}
}
private fun downloadSingleSnap(paramMap: ParamMap, mediaInfoMap: Map<SplitMediaAssetType, MediaInfo>) {
context.executeAsync {
runCatching { handleOperaMedia(paramMap, mediaInfoMap, true, batchForceAllowDuplicate) }
.onFailure {
context.log.error("Batch download failed", it)
context.shortToast(translations["failed_generic_toast"])
}
}
}
private fun processNextBatchDownload(paramMap: ParamMap, mediaInfoMap: Map<SplitMediaAssetType, MediaInfo>) {
val queue = pendingBatchDownloadIndices ?: return
if (queue.isEmpty()) {
flushPendingMergeAndComplete()
return
}
val currentIndex = paramMap.getStorySnapIndex() ?: -1
if (currentIndex != queue.first()) return
queue.removeAt(0)
downloadSingleSnap(paramMap, mediaInfoMap)
if (queue.isEmpty()) {
flushPendingMergeAndComplete()
} else {
val totalCount = paramMap.getStorySnapTotal()
context.runOnUiThread {
fun tryJump(retryCount: Int = 0) {
val delayMs = if (retryCount == 0) 120L else 220L
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
val jumped = runCatching {
context.feature(OperaStoryOverlay::class).requestJumpToSnap(queue.first(), totalCount)
}.getOrNull() == true
if (!jumped && retryCount < 1) {
tryJump(retryCount + 1)
} else if (!jumped) {
pendingBatchDownloadIndices = null
context.shortToast(translations["batch_download_jump_failed_toast"])
}
}, delayMs)
}
tryJump()
}
}
}
private fun flushPendingMergeAndComplete() {
pendingBatchDownloadIndices = null
context.shortToast(translations["batch_download_complete_toast"])
}
fun showLastOperaDebugMediaInfo() {
if (lastSeenMapParams == null || lastSeenMediaInfoMap == null) return
@@ -1063,15 +852,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
lastSeenMapParams = mediaParamMap
lastSeenMediaInfoMap = mediaInfoMap
if (pendingBatchDownloadIndices != null) {
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
if (pendingBatchDownloadIndices != null) {
processNextBatchDownload(mediaParamMap, mediaInfoMap)
}
}, 80L)
return@onOperaViewStateCallback
}
if (!shouldAutoDownload) {
return@onOperaViewStateCallback
}

View File

@@ -1,87 +1,244 @@
package me.eternal.purrfectsnap.core.features.impl.messaging
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioManager
import android.media.ToneGenerator
import android.media.AudioTrack
import kotlinx.coroutines.delay
import me.eternal.purrfectsnap.core.event.events.impl.ConversationUpdateEvent
import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent
import me.eternal.purrfectsnap.core.features.Feature
import kotlin.math.PI
import kotlin.math.exp
import kotlin.math.sin
class ConversationSoundEffects : Feature("Conversation Sound Effects") {
private val seenIncomingMessageIds = LinkedHashSet<Long>()
private val maxTrackedMessages = 512
private data class ToneStep(
val tone: Int,
private data class BubbleSpec(
val durationMs: Int,
val startFreqHz: Double,
val endFreqHz: Double,
val overtoneFreqHz: Double,
val amplitude: Double
)
private data class BubbleStep(
val spec: BubbleSpec,
val pauseAfterMs: Long = 0L
)
private data class ToneSpec(
val sendPattern: List<ToneStep>,
val receivePattern: List<ToneStep>
private val iMessageSendBubble = BubbleSpec(
durationMs = 78,
startFreqHz = 1160.0,
endFreqHz = 690.0,
overtoneFreqHz = 1820.0,
amplitude = 0.50
)
private val iMessageReceiveBubble = BubbleSpec(
durationMs = 92,
startFreqHz = 1040.0,
endFreqHz = 640.0,
overtoneFreqHz = 1680.0,
amplitude = 0.46
)
private val whatsappSendBubble = BubbleSpec(
durationMs = 86,
startFreqHz = 860.0,
endFreqHz = 520.0,
overtoneFreqHz = 1410.0,
amplitude = 0.52
)
private val whatsappReceiveBubble = BubbleSpec(
durationMs = 94,
startFreqHz = 920.0,
endFreqHz = 560.0,
overtoneFreqHz = 1520.0,
amplitude = 0.50
)
private val telegramSendPrimary = BubbleSpec(
durationMs = 58,
startFreqHz = 1110.0,
endFreqHz = 820.0,
overtoneFreqHz = 1710.0,
amplitude = 0.42
)
private val telegramSendAccent = BubbleSpec(
durationMs = 34,
startFreqHz = 1360.0,
endFreqHz = 980.0,
overtoneFreqHz = 2060.0,
amplitude = 0.22
)
private val telegramReceivePrimary = BubbleSpec(
durationMs = 72,
startFreqHz = 1080.0,
endFreqHz = 780.0,
overtoneFreqHz = 1680.0,
amplitude = 0.44
)
private val telegramReceiveAccent = BubbleSpec(
durationMs = 42,
startFreqHz = 1280.0,
endFreqHz = 940.0,
overtoneFreqHz = 1940.0,
amplitude = 0.18
)
private val subtleSendBubble = BubbleSpec(
durationMs = 60,
startFreqHz = 760.0,
endFreqHz = 520.0,
overtoneFreqHz = 1180.0,
amplitude = 0.26
)
private val subtleReceiveBubble = BubbleSpec(
durationMs = 66,
startFreqHz = 800.0,
endFreqHz = 560.0,
overtoneFreqHz = 1260.0,
amplitude = 0.24
)
private fun currentConversationId() = context.feature(Messaging::class).openedConversationUUID?.toString()
private fun styleSpec(): ToneSpec {
return when (context.config.messaging.conversationSoundEffectsStyle.get()) {
"telegram" -> ToneSpec(
sendPattern = listOf(
ToneStep(ToneGenerator.TONE_PROP_BEEP, 35),
ToneStep(ToneGenerator.TONE_PROP_BEEP2, 45, 25)
),
receivePattern = listOf(
ToneStep(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 70),
ToneStep(ToneGenerator.TONE_PROP_BEEP2, 35, 20)
)
)
"whatsapp" -> ToneSpec(
sendPattern = listOf(
ToneStep(ToneGenerator.TONE_PROP_ACK, 55)
),
receivePattern = listOf(
ToneStep(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 85),
ToneStep(ToneGenerator.TONE_PROP_ACK, 35, 15)
)
)
"subtle" -> ToneSpec(
sendPattern = listOf(
ToneStep(ToneGenerator.TONE_PROP_PROMPT, 22)
),
receivePattern = listOf(
ToneStep(ToneGenerator.TONE_PROP_ACK, 28)
)
)
else -> ToneSpec(
sendPattern = listOf(
ToneStep(ToneGenerator.TONE_PROP_PROMPT, 40),
ToneStep(ToneGenerator.TONE_PROP_BEEP, 28, 18)
),
receivePattern = listOf(
ToneStep(ToneGenerator.TONE_PROP_ACK, 55),
ToneStep(ToneGenerator.TONE_PROP_BEEP2, 40, 22)
)
private fun buildBubblePcm(spec: BubbleSpec, sampleRate: Int = 44_100): ByteArray {
val sampleCount = (sampleRate * (spec.durationMs / 1000.0)).toInt().coerceAtLeast(1)
val pcm = ByteArray(sampleCount * 2)
for (i in 0 until sampleCount) {
val progress = i.toDouble() / sampleCount.toDouble()
val envelope = exp(-4.8 * progress) * (1.0 - exp(-20.0 * progress))
val freq = spec.startFreqHz + (spec.endFreqHz - spec.startFreqHz) * progress
val t = i.toDouble() / sampleRate.toDouble()
val fundamental = sin(2.0 * PI * freq * t)
val overtone = 0.18 * sin(2.0 * PI * spec.overtoneFreqHz * t)
val airyTail = 0.08 * sin(2.0 * PI * (freq * 0.48) * t)
val warmth = 0.14 * sin(2.0 * PI * (freq * 0.24) * t)
val sample = ((fundamental + overtone + airyTail + warmth) * envelope * spec.amplitude)
.coerceIn(-1.0, 1.0)
val shortValue = (sample * Short.MAX_VALUE).toInt().toShort()
pcm[i * 2] = (shortValue.toInt() and 0xFF).toByte()
pcm[i * 2 + 1] = ((shortValue.toInt() shr 8) and 0xFF).toByte()
}
return pcm
}
private fun playBubble(spec: BubbleSpec) {
if (context.isMainActivityPaused) return
context.executeAsync {
val sampleRate = 44_100
val pcm = buildBubblePcm(spec, sampleRate)
val audioTrack = AudioTrack(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build(),
AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRate)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build(),
pcm.size,
AudioTrack.MODE_STATIC,
AudioManager.AUDIO_SESSION_ID_GENERATE
)
runCatching {
audioTrack.write(pcm, 0, pcm.size)
audioTrack.play()
delay(spec.durationMs.toLong() + 24L)
}.also {
runCatching {
audioTrack.stop()
audioTrack.release()
}
}
}
}
private fun playPattern(pattern: List<ToneStep>) {
private fun playBubbleSequence(steps: List<BubbleStep>) {
if (context.isMainActivityPaused) return
context.executeAsync {
var toneGenerator: ToneGenerator? = null
runCatching {
toneGenerator = ToneGenerator(AudioManager.STREAM_NOTIFICATION, 55)
pattern.forEach { step ->
toneGenerator?.startTone(step.tone, step.durationMs)
if (step.pauseAfterMs > 0) delay(step.pauseAfterMs)
steps.forEach { step ->
val sampleRate = 44_100
val pcm = buildBubblePcm(step.spec, sampleRate)
val audioTrack = AudioTrack(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build(),
AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRate)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build(),
pcm.size,
AudioTrack.MODE_STATIC,
AudioManager.AUDIO_SESSION_ID_GENERATE
)
runCatching {
audioTrack.write(pcm, 0, pcm.size)
audioTrack.play()
delay(step.spec.durationMs.toLong() + step.pauseAfterMs + 18L)
}.also {
runCatching {
audioTrack.stop()
audioTrack.release()
}
}
}.also {
runCatching { toneGenerator?.release() }
}
}
}
private fun playStyledSend() {
when (context.config.messaging.conversationSoundEffectsStyle.get()) {
"imessage" -> playBubble(iMessageSendBubble)
"whatsapp" -> playBubble(whatsappSendBubble)
"telegram" -> playBubbleSequence(
listOf(
BubbleStep(telegramSendPrimary, pauseAfterMs = 16L),
BubbleStep(telegramSendAccent)
)
)
else -> playBubble(subtleSendBubble)
}
}
private fun playStyledReceive() {
when (context.config.messaging.conversationSoundEffectsStyle.get()) {
"imessage" -> playBubble(iMessageReceiveBubble)
"whatsapp" -> playBubbleSequence(
listOf(
BubbleStep(whatsappReceiveBubble, pauseAfterMs = 12L),
BubbleStep(
whatsappReceiveBubble.copy(
durationMs = 42,
startFreqHz = 1210.0,
endFreqHz = 860.0,
overtoneFreqHz = 1980.0,
amplitude = 0.20
)
)
)
)
"telegram" -> playBubbleSequence(
listOf(
BubbleStep(telegramReceivePrimary, pauseAfterMs = 14L),
BubbleStep(telegramReceiveAccent)
)
)
else -> playBubble(subtleReceiveBubble)
}
}
private fun markSeen(messageId: Long): Boolean {
synchronized(seenIncomingMessageIds) {
val added = seenIncomingMessageIds.add(messageId)
@@ -93,15 +250,14 @@ class ConversationSoundEffects : Feature("Conversation Sound Effects") {
}
override fun init() {
if (!context.config.messaging.conversationSoundEffects.get()) return
if (context.config.messaging.conversationSoundEffectsStyle.get() == "disabled") return
context.event.subscribe(SendMessageWithContentEvent::class) { event ->
val activeConversationId = currentConversationId() ?: return@subscribe
if (event.destinations.conversations?.none { it.toString() == activeConversationId } != false) return@subscribe
event.addCallbackResult("onSuccess") {
val spec = styleSpec()
playPattern(spec.sendPattern)
playStyledSend()
}
}
@@ -110,7 +266,6 @@ class ConversationSoundEffects : Feature("Conversation Sound Effects") {
if (event.conversationId != activeConversationId) return@subscribe
val myUserId = context.database.myUserId ?: return@subscribe
val spec = styleSpec()
event.messages
.asSequence()
@@ -119,7 +274,7 @@ class ConversationSoundEffects : Feature("Conversation Sound Effects") {
.filter { markSeen(it) }
.firstOrNull()
?.let {
playPattern(spec.receivePattern)
playStyledReceive()
}
}
}

View File

@@ -121,11 +121,6 @@ class CameraTweaks : Feature("Camera Tweaks") {
param.setArg(1, captureResolutionConfig[1])
}
CaptureRequest.Builder::class.java.hook("set", HookStage.BEFORE) { param ->
val key = param.arg<CaptureRequest.Key<*>>(0)
if (key == CaptureRequest.CONTROL_ZOOM_RATIO) return@hook
}
CameraCharacteristics::class.java.hook("get", HookStage.AFTER) { param ->
val key = param.argNullable<Key<*>>(0) ?: return@hook
@@ -151,6 +146,14 @@ class CameraTweaks : Feature("Camera Tweaks") {
}
}
}
if (key == CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) {
val isFrontCamera = param.invokeOriginal(
arrayOf(CameraCharacteristics.LENS_FACING)
) == CameraCharacteristics.LENS_FACING_FRONT
val customFrameRate = (if (isFrontCamera) config.frontCustomFrameRate.getNullable() else config.backCustomFrameRate.getNullable())?.toIntOrNull() ?: return@hook
param.setResult(arrayOf(Range(customFrameRate, customFrameRate)))
}
}
if (config.blackPhotos.get()) {

View File

@@ -7,8 +7,8 @@ org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
nativeAbis=arm64-v8a
APP_VERSION_NAME=1.6.0
APP_VERSION_CODE=310
APP_VERSION_NAME=1.6.1
APP_VERSION_CODE=312
debug_build_hash=18fe2a814d0e2eb5
psIntegrityPinnedSha256=
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c