fix custom emoji & call recorder quality,add order feature for chat exports
This commit is contained in:
@@ -232,6 +232,7 @@ class FFMpegProcessor(
|
||||
}
|
||||
globalArguments += "-ar" to args.audioStreamFormat.sampleRate.toString()
|
||||
globalArguments += "-ac" to args.audioStreamFormat.channels.toString()
|
||||
outputArguments += "-c:a" to "pcm_s16le"
|
||||
}
|
||||
Action.MERGE_AUDIO_STREAMS -> {
|
||||
inputArguments.clear()
|
||||
@@ -240,18 +241,23 @@ class FFMpegProcessor(
|
||||
args.inputs.forEachIndexed { index, input ->
|
||||
inputArguments += "-i" to input
|
||||
val offset = args.inputDelayOffsets?.get(input) ?: 0L
|
||||
filterParts.append("[$index:a]aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo")
|
||||
if (offset > 0) {
|
||||
filterParts.append("[$index:a]adelay=$offset|$offset[a$index];")
|
||||
filterParts.append(",adelay=$offset|$offset[a$index];")
|
||||
} else {
|
||||
filterParts.append("[$index:a]acopy[a$index];")
|
||||
filterParts.append(",acopy[a$index];")
|
||||
}
|
||||
}
|
||||
args.inputs.indices.forEach { index ->
|
||||
filterParts.append("[a$index]")
|
||||
}
|
||||
filterParts.append("amix=inputs=${args.inputs.size}:duration=longest:normalize=0[aout]")
|
||||
filterParts.append("amix=inputs=${args.inputs.size}:duration=longest:dropout_transition=0:normalize=1,alimiter=limit=0.95[aout]")
|
||||
outputArguments += "-filter_complex" to "\"$filterParts\""
|
||||
outputArguments += "-map" to "\"[aout]\""
|
||||
outputArguments += "-c:a" to "libmp3lame"
|
||||
outputArguments += "-b:a" to "192k"
|
||||
outputArguments += "-ar" to "48000"
|
||||
outputArguments += "-ac" to "2"
|
||||
}
|
||||
}
|
||||
outputArguments += args.output.absolutePath
|
||||
|
||||
@@ -46,7 +46,7 @@ class CallDownloadSessionImpl(
|
||||
val job: Job
|
||||
val writePfd: ParcelFileDescriptor
|
||||
|
||||
val outputFile = context.androidContext.cacheDir.resolve("call_${UUID.randomUUID()}.mp3").apply {
|
||||
val outputFile = context.androidContext.cacheDir.resolve("call_${UUID.randomUUID()}.wav").apply {
|
||||
if (exists()) delete()
|
||||
}
|
||||
|
||||
|
||||
@@ -537,7 +537,8 @@
|
||||
},
|
||||
"file_imports": {
|
||||
"no_files_settings_hint": "No files found. Make sure you have imported the required files in the File Imports section",
|
||||
"settings_select_file_hint": "Select an imported file"
|
||||
"settings_select_file_hint": "Select an imported file",
|
||||
"settings_select_file_subtitle": "Choose a file from your imported files list"
|
||||
}
|
||||
},
|
||||
"scripting": {
|
||||
@@ -2910,6 +2911,9 @@
|
||||
"text_field_selection": "{amount} selected",
|
||||
"text_field_selection_all": "All",
|
||||
"export_file_format_title": "Export File Format",
|
||||
"sort_order_title": "Message Order",
|
||||
"sort_order_newest_to_oldest": "Newest to Oldest",
|
||||
"sort_order_oldest_to_newest": "Oldest to Newest",
|
||||
"message_type_filter_title": "Filter Messages by Type",
|
||||
"amount_of_messages_title": "Message Count (leave blank for all)",
|
||||
"download_medias_title": "Download Media"
|
||||
|
||||
@@ -74,6 +74,7 @@ import me.eternal.purrfectsnap.core.logger.CoreLogger
|
||||
import me.eternal.purrfectsnap.core.messaging.ConversationExporter
|
||||
import me.eternal.purrfectsnap.core.messaging.ExportFormat
|
||||
import me.eternal.purrfectsnap.core.messaging.ExportParams
|
||||
import me.eternal.purrfectsnap.core.messaging.ExportSortOrder
|
||||
import me.eternal.purrfectsnap.core.wrapper.impl.Message
|
||||
import java.io.File
|
||||
import kotlin.math.absoluteValue
|
||||
@@ -180,7 +181,9 @@ class ExportChatMessages : AbstractAction() {
|
||||
var downloadMedias by remember { mutableStateOf(false) }
|
||||
var showConversationPicker by remember { mutableStateOf(false) }
|
||||
var showFormatPicker by remember { mutableStateOf(false) }
|
||||
var showOrderPicker by remember { mutableStateOf(false) }
|
||||
var showMessageTypePicker by remember { mutableStateOf(false) }
|
||||
var exportSortOrder by remember { mutableStateOf(ExportSortOrder.NEWEST_TO_OLDEST) }
|
||||
val colorOverrides = remember { mutableStateMapOf<String, String>() }
|
||||
var colorPickerTarget by remember { mutableStateOf<ExportColorParticipant?>(null) }
|
||||
var colorPickerValue by remember { mutableStateOf<Color?>(null) }
|
||||
@@ -309,6 +312,19 @@ class ExportChatMessages : AbstractAction() {
|
||||
tint = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
|
||||
SectionLabel(t("sort_order_title"))
|
||||
GlassField(
|
||||
value = t(
|
||||
if (exportSortOrder == ExportSortOrder.OLDEST_TO_NEWEST) {
|
||||
"sort_order_oldest_to_newest"
|
||||
} else {
|
||||
"sort_order_newest_to_oldest"
|
||||
}
|
||||
),
|
||||
onClick = { showOrderPicker = true },
|
||||
tint = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
|
||||
SectionLabel(t("message_type_filter_title"))
|
||||
GlassField(
|
||||
value = messageTypeFilter.takeIf { it.isNotEmpty() }?.let {
|
||||
@@ -417,6 +433,7 @@ class ExportChatMessages : AbstractAction() {
|
||||
selection,
|
||||
ExportParams(
|
||||
exportFormat = exportType,
|
||||
sortOrder = exportSortOrder,
|
||||
messageTypeFilter = messageTypeFilter.takeIf { it.isNotEmpty() },
|
||||
amountOfMessages = amountOfMessages.takeIf { it != -1 },
|
||||
downloadMedias = downloadMedias,
|
||||
@@ -610,6 +627,38 @@ class ExportChatMessages : AbstractAction() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showOrderPicker) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.55f))
|
||||
.clickable { showOrderPicker = false }
|
||||
)
|
||||
SelectorPopup(
|
||||
title = t("sort_order_title"),
|
||||
onDismiss = { showOrderPicker = false }
|
||||
) {
|
||||
SelectorRow(
|
||||
title = t("sort_order_newest_to_oldest"),
|
||||
subtitle = null,
|
||||
checked = exportSortOrder == ExportSortOrder.NEWEST_TO_OLDEST,
|
||||
onToggle = {
|
||||
exportSortOrder = ExportSortOrder.NEWEST_TO_OLDEST
|
||||
showOrderPicker = false
|
||||
}
|
||||
)
|
||||
SelectorRow(
|
||||
title = t("sort_order_oldest_to_newest"),
|
||||
subtitle = null,
|
||||
checked = exportSortOrder == ExportSortOrder.OLDEST_TO_NEWEST,
|
||||
onToggle = {
|
||||
exportSortOrder = ExportSortOrder.OLDEST_TO_NEWEST
|
||||
showOrderPicker = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1159,6 +1208,13 @@ class ExportChatMessages : AbstractAction() {
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
private fun messageSortKey(message: Message): Long {
|
||||
return message.orderKey
|
||||
?: message.messageMetadata?.createdAt
|
||||
?: message.messageDescriptor?.messageId
|
||||
?: Long.MIN_VALUE
|
||||
}
|
||||
|
||||
private suspend fun exportFullConversation(
|
||||
feedEntry: FriendFeedEntry,
|
||||
exportParams: ExportParams,
|
||||
@@ -1209,58 +1265,75 @@ class ExportChatMessages : AbstractAction() {
|
||||
|
||||
var foundMessageCount = 0
|
||||
val exportedOrderKeys = mutableSetOf<Long>()
|
||||
val fetchedMessages = mutableListOf<Message>()
|
||||
val seenMessageKeys = mutableSetOf<String>()
|
||||
|
||||
var lastMessageId: Long? = null
|
||||
fetchMessagesPaginated(conversationId, Long.MAX_VALUE, amount = 1).firstOrNull()?.also { message ->
|
||||
conversationExporter.readMessage(message)
|
||||
foundMessageCount++
|
||||
message.orderKey?.let { exportedOrderKeys.add(it) }
|
||||
val messageKey = message.orderKey?.toString() ?: message.messageDescriptor?.messageId?.toString()
|
||||
if (messageKey != null && seenMessageKeys.add(messageKey)) {
|
||||
fetchedMessages.add(message)
|
||||
}
|
||||
lastMessageId = message.messageDescriptor?.messageId
|
||||
}
|
||||
|
||||
if (lastMessageId == null) {
|
||||
if (lastMessageId == null && fetchedMessages.isEmpty()) {
|
||||
logDialog(translation["no_messages_found"])
|
||||
}
|
||||
|
||||
while (lastMessageId != null) {
|
||||
val fetchedMessages = fetchMessagesPaginated(conversationId, lastMessageId, amount = 500).toMutableList()
|
||||
if (fetchedMessages.isEmpty()) break
|
||||
val pagedMessages = fetchMessagesPaginated(conversationId, lastMessageId, amount = 500)
|
||||
if (pagedMessages.isEmpty()) break
|
||||
|
||||
fetchedMessages.firstOrNull()?.let {
|
||||
pagedMessages.firstOrNull()?.let {
|
||||
lastMessageId = it.messageDescriptor!!.messageId!!
|
||||
}
|
||||
|
||||
exportParams.messageTypeFilter?.let { filter ->
|
||||
fetchedMessages.removeIf { message ->
|
||||
!filter.contains(message.messageContent?.contentType ?: return@removeIf false)
|
||||
pagedMessages.forEach { message ->
|
||||
val messageKey = message.orderKey?.toString() ?: message.messageDescriptor?.messageId?.toString()
|
||||
if (messageKey != null && seenMessageKeys.add(messageKey)) {
|
||||
fetchedMessages.add(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val remainingLimit = exportParams.amountOfMessages?.let { it - foundMessageCount } ?: Int.MAX_VALUE
|
||||
if (remainingLimit <= 0) break
|
||||
|
||||
val messagesToWrite = fetchedMessages.reversed().let { messages ->
|
||||
if (messages.size <= remainingLimit) messages else messages.subList(0, remainingLimit)
|
||||
val filteredMessages = exportParams.messageTypeFilter?.let { filter ->
|
||||
fetchedMessages.filter { message ->
|
||||
val contentType = message.messageContent?.contentType ?: return@filter false
|
||||
filter.contains(contentType)
|
||||
}
|
||||
} ?: fetchedMessages
|
||||
|
||||
messagesToWrite.forEach { message ->
|
||||
conversationExporter.readMessage(message)
|
||||
foundMessageCount++
|
||||
message.orderKey?.let { exportedOrderKeys.add(it) }
|
||||
}
|
||||
val sortedMessages = when (exportParams.sortOrder) {
|
||||
ExportSortOrder.OLDEST_TO_NEWEST -> filteredMessages.sortedBy { messageSortKey(it) }
|
||||
ExportSortOrder.NEWEST_TO_OLDEST -> filteredMessages.sortedByDescending { messageSortKey(it) }
|
||||
}
|
||||
|
||||
val messagesToWrite = exportParams.amountOfMessages?.let { limit ->
|
||||
sortedMessages.take(limit)
|
||||
} ?: sortedMessages
|
||||
|
||||
messagesToWrite.forEach { message ->
|
||||
conversationExporter.readMessage(message)
|
||||
foundMessageCount++
|
||||
message.orderKey?.let { exportedOrderKeys.add(it) }
|
||||
setStatus("Exporting (found ${foundMessageCount})")
|
||||
}
|
||||
|
||||
if (loggerMessages.isNotEmpty() && (exportParams.amountOfMessages == null || foundMessageCount < exportParams.amountOfMessages)) {
|
||||
val parsedLoggerMessages = loggerMessages.mapNotNull { conversationExporter.parseLoggedMessage(it) }
|
||||
for (loggedMessage in parsedLoggerMessages.asReversed()) {
|
||||
val sortedLoggerMessages = when (exportParams.sortOrder) {
|
||||
ExportSortOrder.OLDEST_TO_NEWEST -> parsedLoggerMessages.sortedBy { it.orderKey }
|
||||
ExportSortOrder.NEWEST_TO_OLDEST -> parsedLoggerMessages.sortedByDescending { it.orderKey }
|
||||
}
|
||||
for (loggedMessage in sortedLoggerMessages) {
|
||||
if (exportedOrderKeys.contains(loggedMessage.orderKey)) continue
|
||||
val filter = exportParams.messageTypeFilter
|
||||
if (filter != null && !filter.contains(loggedMessage.contentType)) continue
|
||||
if (exportParams.amountOfMessages != null && foundMessageCount >= exportParams.amountOfMessages) break
|
||||
conversationExporter.readLoggedMessage(loggedMessage)
|
||||
foundMessageCount++
|
||||
setStatus("Exporting (found ${foundMessageCount})")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +184,29 @@ class CallRecorder : Feature("Call Recorder") {
|
||||
onCallStarted(conversationId)
|
||||
}
|
||||
|
||||
private fun clampCopyRange(offset: Int, requestedLength: Int, maxLength: Int): Pair<Int, Int>? {
|
||||
if (requestedLength <= 0 || maxLength <= 0) return null
|
||||
val safeOffset = offset.coerceAtLeast(0).coerceAtMost(maxLength)
|
||||
val available = (maxLength - safeOffset).coerceAtLeast(0)
|
||||
val safeLength = requestedLength.coerceAtMost(available)
|
||||
if (safeLength <= 0) return null
|
||||
return safeOffset to safeLength
|
||||
}
|
||||
|
||||
private fun copyAudioRecordByteBuffer(data: ByteBuffer, bytesRead: Int): ByteArray? {
|
||||
if (bytesRead <= 0) return null
|
||||
val currentPosition = data.position()
|
||||
val start = (currentPosition - bytesRead).coerceAtLeast(0)
|
||||
val end = currentPosition.coerceAtMost(data.limit())
|
||||
if (end <= start) return null
|
||||
return ByteArray(end - start).also { out ->
|
||||
val dup = data.duplicate()
|
||||
dup.position(start)
|
||||
dup.limit(end)
|
||||
dup.get(out)
|
||||
}
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
if (callRecorderConfig.callRecorder.getNullable() == null) return
|
||||
|
||||
@@ -221,10 +244,18 @@ class CallRecorder : Feature("Call Recorder") {
|
||||
val wrapper = streams[param.thisObject<Any>().hashCode()] ?: return@hook
|
||||
|
||||
val buffer = when (val data = param.arg<Any>(0)) {
|
||||
is ByteBuffer -> ByteArray(result).also { val pos = data.position(); data.get(it); data.position(pos) }
|
||||
is ByteArray -> data.copyOfRange(param.argNullable(1) ?: 0, (param.argNullable<Int>(1) ?: 0) + result)
|
||||
is ShortArray -> ByteArray(result * 2).also {
|
||||
ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(data, param.argNullable(1) ?: 0, result)
|
||||
is ByteBuffer -> copyAudioRecordByteBuffer(data, result) ?: return@hook
|
||||
is ByteArray -> {
|
||||
val offset = param.argNullable<Int>(1) ?: 0
|
||||
val (safeOffset, safeLength) = clampCopyRange(offset, result, data.size) ?: return@hook
|
||||
data.copyOfRange(safeOffset, safeOffset + safeLength)
|
||||
}
|
||||
is ShortArray -> {
|
||||
val offset = param.argNullable<Int>(1) ?: 0
|
||||
val (safeOffset, safeLength) = clampCopyRange(offset, result, data.size) ?: return@hook
|
||||
ByteArray(safeLength * 2).also {
|
||||
ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(data, safeOffset, safeLength)
|
||||
}
|
||||
}
|
||||
else -> return@hook
|
||||
}
|
||||
@@ -260,14 +291,31 @@ class CallRecorder : Feature("Call Recorder") {
|
||||
hook("write", HookStage.BEFORE) { param ->
|
||||
val wrapper = streams[param.thisObject<Any>().hashCode()] ?: return@hook
|
||||
val data = param.arg<Any>(0)
|
||||
val size = if (param.args().size > 2) param.arg(2) else if (data is ByteArray) data.size else if (data is ShortArray) data.size else if (data is ByteBuffer) data.remaining() else 0
|
||||
if (size <= 0) return@hook
|
||||
|
||||
val buffer = when (data) {
|
||||
is ByteBuffer -> ByteArray(size).also { val pos = data.position(); data.get(it); data.position(pos) }
|
||||
is ByteArray -> data.copyOfRange(param.argNullable(1) ?: 0, (param.argNullable<Int>(1) ?: 0) + size)
|
||||
is ShortArray -> ByteArray(size * 2).also {
|
||||
ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(data, param.argNullable(1) ?: 0, size)
|
||||
is ByteBuffer -> {
|
||||
val requestedSize = param.argNullable<Int>(1) ?: data.remaining()
|
||||
val safeSize = requestedSize.coerceAtMost(data.remaining()).coerceAtLeast(0)
|
||||
if (safeSize <= 0) return@hook
|
||||
ByteArray(safeSize).also {
|
||||
val pos = data.position()
|
||||
data.get(it, 0, safeSize)
|
||||
data.position(pos)
|
||||
}
|
||||
}
|
||||
is ByteArray -> {
|
||||
val offset = param.argNullable<Int>(1) ?: 0
|
||||
val requestedSize = param.argNullable<Int>(2) ?: data.size
|
||||
val (safeOffset, safeLength) = clampCopyRange(offset, requestedSize, data.size) ?: return@hook
|
||||
data.copyOfRange(safeOffset, safeOffset + safeLength)
|
||||
}
|
||||
is ShortArray -> {
|
||||
val offset = param.argNullable<Int>(1) ?: 0
|
||||
val requestedSize = param.argNullable<Int>(2) ?: data.size
|
||||
val (safeOffset, safeLength) = clampCopyRange(offset, requestedSize, data.size) ?: return@hook
|
||||
ByteArray(safeLength * 2).also {
|
||||
ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(data, safeOffset, safeLength)
|
||||
}
|
||||
}
|
||||
else -> return@hook
|
||||
}
|
||||
|
||||
@@ -1,52 +1,84 @@
|
||||
package me.eternal.purrfectsnap.core.features.impl.experiments
|
||||
|
||||
import android.graphics.Typeface
|
||||
import me.eternal.purrfectsnap.common.bridge.FileHandleScope
|
||||
import me.eternal.purrfectsnap.core.ModContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
|
||||
private var cacheFontPath: String? = null
|
||||
private var cacheFontName: String? = null
|
||||
private var cachedFontPath: String? = null
|
||||
private var cachedFontName: String? = null
|
||||
|
||||
fun getCustomEmojiFontPath(
|
||||
context: ModContext
|
||||
): String? {
|
||||
val customFileName = context.config.experimental.nativeHooks.customEmojiFont.getNullable()?.takeIf { it.isNotBlank() } ?: return null
|
||||
val safeFileName = customFileName.substringAfterLast("/")
|
||||
val customFileName = context.config.experimental.nativeHooks.customEmojiFont.getNullable()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotBlank() } ?: return null
|
||||
val safeFileName = customFileName.substringAfterLast("/").substringAfterLast("\\")
|
||||
if (safeFileName.isBlank()) return null
|
||||
|
||||
fun clearCachedPath(cacheFile: File? = null): String? {
|
||||
runCatching { cacheFile?.delete() }
|
||||
cachedFontPath = null
|
||||
cachedFontName = safeFileName
|
||||
return null
|
||||
}
|
||||
|
||||
fun validateFontFile(file: File): Boolean {
|
||||
return runCatching {
|
||||
Typeface.createFromFile(file)
|
||||
true
|
||||
}.onFailure {
|
||||
context.log.warn("Custom emoji font validation failed for ${file.absolutePath}: ${it.message}")
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
val handle = context.fileHandlerManager.getFileHandle(
|
||||
FileHandleScope.USER_IMPORT.key,
|
||||
customFileName
|
||||
) ?: return@runCatching null
|
||||
) ?: return@runCatching clearCachedPath()
|
||||
if (!handle.exists()) {
|
||||
File(context.androidContext.filesDir, "emoji_fonts")
|
||||
.resolve(safeFileName)
|
||||
.takeIf { it.exists() }
|
||||
?.delete()
|
||||
cacheFontPath = ""
|
||||
cacheFontName = safeFileName
|
||||
return@runCatching null
|
||||
val oldCacheFile = File(context.androidContext.cacheDir, "emoji_fonts").resolve(safeFileName)
|
||||
return@runCatching clearCachedPath(oldCacheFile)
|
||||
}
|
||||
|
||||
val persistentDir = File(context.androidContext.filesDir, "emoji_fonts").apply {
|
||||
val persistentDir = File(context.androidContext.cacheDir, "emoji_fonts").apply {
|
||||
mkdirs()
|
||||
}
|
||||
val persistentFile = File(persistentDir, safeFileName)
|
||||
handle.open(android.os.ParcelFileDescriptor.MODE_READ_ONLY)?.use { pfd ->
|
||||
if (!persistentFile.exists() || pfd.statSize != persistentFile.length()) {
|
||||
FileOutputStream(persistentFile).use { output ->
|
||||
|
||||
val needsCopy = handle.open(android.os.ParcelFileDescriptor.MODE_READ_ONLY)?.use { pfd ->
|
||||
val sourceSize = pfd.statSize
|
||||
!persistentFile.exists() || (sourceSize > 0 && sourceSize != persistentFile.length())
|
||||
} ?: true
|
||||
|
||||
if (needsCopy) {
|
||||
val tempFile = File(persistentDir, "$safeFileName.tmp")
|
||||
handle.open(android.os.ParcelFileDescriptor.MODE_READ_ONLY)?.use { pfd ->
|
||||
FileOutputStream(tempFile).use { output ->
|
||||
android.os.ParcelFileDescriptor.AutoCloseInputStream(pfd).use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!tempFile.renameTo(persistentFile)) {
|
||||
tempFile.copyTo(persistentFile, overwrite = true)
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
cacheFontName = safeFileName
|
||||
cacheFontPath = persistentFile.absolutePath
|
||||
cacheFontPath?.takeIf { it.isNotEmpty() }
|
||||
|
||||
if (!validateFontFile(persistentFile)) {
|
||||
return@runCatching clearCachedPath(persistentFile)
|
||||
}
|
||||
|
||||
cachedFontName = safeFileName
|
||||
cachedFontPath = persistentFile.absolutePath
|
||||
cachedFontPath?.takeIf { it.isNotEmpty() }
|
||||
}.onFailure {
|
||||
context.log.error("Failed to get custom emoji font", it)
|
||||
clearCachedPath()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,14 @@ package me.eternal.purrfectsnap.core.messaging
|
||||
|
||||
import me.eternal.purrfectsnap.common.data.ContentType
|
||||
|
||||
enum class ExportSortOrder {
|
||||
NEWEST_TO_OLDEST,
|
||||
OLDEST_TO_NEWEST
|
||||
}
|
||||
|
||||
class ExportParams(
|
||||
val exportFormat: ExportFormat = ExportFormat.HTML,
|
||||
val sortOrder: ExportSortOrder = ExportSortOrder.NEWEST_TO_OLDEST,
|
||||
val messageTypeFilter: List<ContentType>? = null,
|
||||
val amountOfMessages: Int? = null,
|
||||
val downloadMedias: Boolean = false,
|
||||
|
||||
@@ -11,6 +11,16 @@ object LSPatchUpdater {
|
||||
var HAS_LSPATCH = false
|
||||
private set
|
||||
|
||||
private fun ensureTranslationsLoaded(context: ModContext) {
|
||||
if (context.translation.getOrNull("toast_purrfectsnap_updated") != null) return
|
||||
runCatching {
|
||||
context.translation.userLocale = context.getConfigLocale()
|
||||
context.translation.load()
|
||||
}.onFailure {
|
||||
context.log.warn("Failed to load translations in updater: ${it.message}", TAG)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getModuleUniqueHash(module: ZipFile): String {
|
||||
return module.entries().asSequence()
|
||||
.filter { !it.isDirectory }
|
||||
@@ -20,6 +30,8 @@ object LSPatchUpdater {
|
||||
}
|
||||
|
||||
fun onBridgeConnected(context: ModContext) {
|
||||
ensureTranslationsLoaded(context)
|
||||
|
||||
val obfuscatedModulePath by lazy {
|
||||
(runCatching {
|
||||
context::class.java.classLoader?.loadClass("org.lsposed.lspatch.share.Constants")
|
||||
@@ -59,19 +71,19 @@ object LSPatchUpdater {
|
||||
}
|
||||
|
||||
context.log.verbose("updating", TAG)
|
||||
context.shortToast(context.translation["toast_updating_purrfectsnap"])
|
||||
context.shortToast(context.translation.getOrNull("toast_updating_purrfectsnap") ?: "Updating PurrfectSnap. Please wait...")
|
||||
// copy embedded module to cache
|
||||
runCatching {
|
||||
seAppApk.copyTo(embeddedModule, overwrite = true)
|
||||
}.onFailure {
|
||||
seAppApk.delete()
|
||||
context.log.error("Failed to copy embedded module", it, TAG)
|
||||
context.longToast(context.translation["toast_update_purrfectsnap_failed"])
|
||||
context.longToast(context.translation.getOrNull("toast_update_purrfectsnap_failed") ?: "Failed to update PurrfectSnap. Please check logcat for more details.")
|
||||
context.forceCloseApp()
|
||||
return
|
||||
}
|
||||
|
||||
context.longToast(context.translation["toast_purrfectsnap_updated"])
|
||||
context.longToast(context.translation.getOrNull("toast_purrfectsnap_updated") ?: "PurrfectSnap updated!")
|
||||
context.log.verbose("updated", TAG)
|
||||
context.softRestartApp()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{ffi::CStr, fs};
|
||||
use std::{ffi::{CStr, CString}, fs};
|
||||
|
||||
use nix::libc::{self, c_uint};
|
||||
|
||||
@@ -12,7 +12,18 @@ def_hook!(
|
||||
if pathname == "/system/fonts/NotoColorEmoji.ttf" {
|
||||
if let Some(font_path) = config::native_config().custom_emoji_font_path {
|
||||
if fs::metadata(&font_path).is_ok() {
|
||||
return libc::openat(libc::AT_FDCWD, font_path.as_ptr() as *const u8, flags, mode);
|
||||
match CString::new(font_path.clone()) {
|
||||
Ok(c_font_path) => {
|
||||
let fd = libc::openat(libc::AT_FDCWD, c_font_path.as_ptr() as *const u8, flags, mode);
|
||||
if fd >= 0 {
|
||||
return fd;
|
||||
}
|
||||
warn!("failed to open custom emoji font path (fd={}): {}", fd, font_path);
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("custom emoji font path contains null byte, using fallback system font");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("custom emoji font path does not exist: {}", font_path);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user