diff --git a/TMessagesProj/build.gradle.kts b/TMessagesProj/build.gradle.kts index 269d0e930..55387fc38 100644 --- a/TMessagesProj/build.gradle.kts +++ b/TMessagesProj/build.gradle.kts @@ -99,6 +99,9 @@ dependencies { implementation(libs.ktor.client.contentNegotiation) implementation(libs.ktor.serialization.json) + implementation(libs.ffmpeg) + implementation(libs.lottie) + implementation(project(":libs:tcp2ws")) implementation(project(":libs:pangu")) ksp(project(":libs:ksp")) diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index 9818aaea3..7d8a8fb45 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -26973,12 +26973,12 @@ public void setAutoDeleteHistory(int time, int action) { options.add(OPTION_ADD_TO_STICKERS_OR_MASKS); icons.add(R.drawable.msg_sticker); } else { - if (!selectedObject.isAnimatedSticker()) { +// if (!selectedObject.isAnimatedSticker()) { items.add(LocaleController.getString("SaveToGallery", R.string.SaveToGallery)); options.add(OPTION_SAVE_STICKER_TO_GALLERY); icons.add(R.drawable.msg_gallery); - } +// } items.add(LocaleController.getString("AddToStickers", R.string.AddToStickers)); options.add(OPTION_ADD_TO_STICKERS_OR_MASKS); @@ -27018,12 +27018,12 @@ public void setAutoDeleteHistory(int time, int action) { icons.add(R.drawable.msg_callback); } } else if (type == 9) { - if (!selectedObject.isAnimatedSticker()) { +// if (!selectedObject.isAnimatedSticker()) { items.add(LocaleController.getString("SaveToGallery", R.string.SaveToGallery)); options.add(OPTION_SAVE_STICKER_TO_GALLERY); icons.add(R.drawable.msg_gallery); - } +// } TLRPC.Document document = selectedObject.getDocument(); if (!getMediaDataController().isStickerInFavorites(document)) { if (MessageObject.isStickerHasSet(document)) { diff --git a/TMessagesProj/src/main/java/xyz/nextalone/nnngram/utils/MessageUtils.kt b/TMessagesProj/src/main/java/xyz/nextalone/nnngram/utils/MessageUtils.kt index 8f2db7ec9..76fdf735a 100644 --- a/TMessagesProj/src/main/java/xyz/nextalone/nnngram/utils/MessageUtils.kt +++ b/TMessagesProj/src/main/java/xyz/nextalone/nnngram/utils/MessageUtils.kt @@ -28,6 +28,7 @@ import android.content.Context import android.content.DialogInterface import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.Canvas import android.net.Uri import android.text.TextUtils import android.util.Base64 @@ -41,7 +42,14 @@ import android.view.inputmethod.EditorInfo import android.widget.FrameLayout import android.widget.TextView import android.widget.TimePicker +import android.widget.Toast import androidx.core.content.FileProvider +import com.airbnb.lottie.LottieComposition +import com.airbnb.lottie.LottieCompositionFactory +import com.airbnb.lottie.LottieDrawable +import com.airbnb.lottie.LottieResult +import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS +import com.arthenica.mobileffmpeg.FFmpeg import com.google.zxing.EncodeHintType import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel @@ -93,6 +101,7 @@ import org.telegram.ui.Components.TranscribeButton import xyz.nextalone.nnngram.helpers.QrHelper import xyz.nextalone.nnngram.helpers.QrHelper.readQr import java.io.File +import java.io.FileInputStream import java.io.FileOutputStream import java.nio.ByteBuffer import java.nio.charset.StandardCharsets @@ -416,7 +425,7 @@ class MessageUtils(num: Int) : BaseController(num) { } fun saveStickerToGallery(activity: Activity, messageObject: MessageObject, callback: Utilities.Callback) { - saveStickerToGallery(activity, getPathToMessage(messageObject), messageObject.isVideoSticker, callback) + saveStickerToGallery(activity, getPathToMessage(messageObject), messageObject.isVideoSticker, messageObject.isAnimatedSticker, callback) } fun addMessageToClipboard(selectedObject: MessageObject, callback: Runnable) { @@ -866,14 +875,94 @@ class MessageUtils(num: Int) : BaseController(num) { if (!temp.exists()) { return } - saveStickerToGallery(activity, path, MessageObject.isVideoSticker(document), callback) + saveStickerToGallery(activity, path, MessageObject.isVideoSticker(document), MessageObject.isAnimatedStickerDocument(document), callback) } - private fun saveStickerToGallery(activity: Activity, path: String?, video: Boolean, callback: Utilities.Callback) { + private fun saveStickerToGallery(activity: Activity, path: String?, video: Boolean, animated: Boolean, callback: Utilities.Callback) { Utilities.globalQueue.postRunnable { tryOrLog { if (video) { - MediaController.saveFile(path, activity, 1, null, null, callback) + val outputPath = + path!!.replace(".webm", ".gif") + if (File(outputPath).exists()) { + File(outputPath).delete() + } + val cmd = arrayOf("-y", "-i", path, "-vf", "colorkey=0x000000:0.1:0.1,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse", outputPath) + FFmpeg.executeAsync(cmd) { executionId, returnCode -> + if (returnCode == RETURN_CODE_SUCCESS) { + MediaController.saveFile(outputPath, activity, 0, null, null, callback) + } else { + Log.e("FFmpeg", "Failed to convert to GIF: $returnCode, file: $path") + Toast.makeText(activity, "Failed to convert to GIF, Use Mp4", Toast.LENGTH_SHORT).show() + com.arthenica.mobileffmpeg.Config.printLastCommandOutput(android.util.Log.ERROR) + MediaController.saveFile(path, activity, 1, null, null, callback) + } + } + } else if (animated) { + CoroutineScope(Dispatchers.IO).launch { + val outputPath = path!!.replace(".tgs", ".gif") + if (File(outputPath).exists()) { + File(outputPath).delete() + } + + val result: LottieResult = LottieCompositionFactory.fromJsonInputStreamSync( + FileInputStream(File(path)), path) + val composition: LottieComposition? = result.value + + composition?.let { comp -> + val lottieDrawable = LottieDrawable().apply { this.composition = comp } + + lottieDrawable.setBounds(0, 0, comp.bounds.width(), comp.bounds.height()) + + val tempDir = File(activity.cacheDir, "temp_${System.currentTimeMillis()}") + if (!tempDir.exists()) { + tempDir.mkdirs() + } + + for (i in comp.startFrame.toInt() until comp.endFrame.toInt()) { + lottieDrawable.frame = i + + val bitmap = Bitmap.createBitmap(comp.bounds.width(), comp.bounds.height(), Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + lottieDrawable.draw(canvas) + + val file = File(tempDir, "$i.png") + FileOutputStream(file).use { fos -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos) + } + } + val generatePaletteCommand = arrayOf( + "-i", "${tempDir.absolutePath}/%d.png", + "-vf", "palettegen=stats_mode=diff", + "-y", "${tempDir.absolutePath}/palette.png" + ) + val createGifCommand = arrayOf( + "-framerate", "60", + "-i", "${tempDir.absolutePath}/%d.png", + "-i", "${tempDir.absolutePath}/palette.png", + "-filter_complex", "[0:v]scale=320:-1:flags=lanczos[v];[v][1:v]paletteuse=dither=none:diff_mode=rectangle", + "-y", outputPath + ) + FFmpeg.executeAsync(generatePaletteCommand) { executionId, returnCode -> + if (returnCode == RETURN_CODE_SUCCESS) { + FFmpeg.executeAsync(createGifCommand) { executionId, returnCode -> + if (returnCode == RETURN_CODE_SUCCESS) { + MediaController.saveFile(outputPath, activity, 0, null, null, callback) + } else { + Log.e("FFmpeg", "Failed to convert to GIF: $returnCode, file: $path") + Toast.makeText(activity, "Failed to convert to GIF, Use tgs", Toast.LENGTH_SHORT).show() + com.arthenica.mobileffmpeg.Config.printLastCommandOutput(android.util.Log.ERROR) + } + tempDir.deleteRecursively() + } + } else { + Log.e("FFmpeg", "Failed to convert to GIF: $returnCode, file: $path") + Toast.makeText(activity, "Failed to convert to GIF, Use tgs", Toast.LENGTH_SHORT).show() + com.arthenica.mobileffmpeg.Config.printLastCommandOutput(android.util.Log.ERROR) + } + } + } + } } else { val image = BitmapFactory.decodeFile(path) if (image != null) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e14db7a6e..b760e7711 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,6 +76,8 @@ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "kto ktor-client-encoding = { module = "io.ktor:ktor-client-encoding", version.ref = "ktor" } ktor-client-contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json-jvm", version.ref = "ktor" } +ffmpeg = { module = "com.arthenica:mobile-ffmpeg-min", version = "4.4.LTS" } +lottie = { module = "com.airbnb.android:lottie", version = "4.1.0" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }