diff --git a/TMessagesProj/build.gradle.kts b/TMessagesProj/build.gradle.kts
index bb3f1f3b71..0df9bd908f 100644
--- a/TMessagesProj/build.gradle.kts
+++ b/TMessagesProj/build.gradle.kts
@@ -97,6 +97,10 @@ dependencies {
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.serialization.json)
+ implementation(files("libs/ffmpeg-kit-video-4.4.LTS.aar"))
+ implementation(libs.smart.exception.java)
+ implementation(libs.lottie)
+
implementation(project(":libs:tcp2ws"))
implementation(project(":libs:pangu"))
ksp(project(":libs:ksp"))
diff --git a/TMessagesProj/libs/ffmpeg-kit-video-4.4.LTS.aar b/TMessagesProj/libs/ffmpeg-kit-video-4.4.LTS.aar
new file mode 100644
index 0000000000..0cf5562583
Binary files /dev/null and b/TMessagesProj/libs/ffmpeg-kit-video-4.4.LTS.aar differ
diff --git a/TMessagesProj/src/main/AndroidManifest.xml b/TMessagesProj/src/main/AndroidManifest.xml
index dd83eab877..5d1ff58723 100644
--- a/TMessagesProj/src/main/AndroidManifest.xml
+++ b/TMessagesProj/src/main/AndroidManifest.xml
@@ -479,7 +479,7 @@
-e
+
diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java
index 0b49a008f7..82992d5f7a 100644
--- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java
+++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java
@@ -29812,6 +29812,11 @@ 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()) {
+ items.add(LocaleController.getString(R.string.SaveToGallery));
+ options.add(OPTION_SAVE_STICKER_TO_GALLERY);
+ icons.add(R.drawable.msg_gallery);
+// }
items.add(LocaleController.getString(R.string.AddToStickers));
options.add(OPTION_ADD_TO_STICKERS_OR_MASKS);
icons.add(R.drawable.msg_sticker);
@@ -29848,12 +29853,11 @@ public void setAutoDeleteHistory(int time, int action) {
icons.add(R.drawable.msg_callback);
}
} else if (type == 9) {
- if (!selectedObject.isAnimatedSticker()) {
- items.add(LocaleController.getString("SaveToGallery",
- R.string.SaveToGallery));
+// if (!selectedObject.isAnimatedSticker()) {
+ items.add(LocaleController.getString(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 (getMediaDataController().canAddStickerToFavorites()) {
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 0afa284093..aeb5756591 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.ffmpegkit.FFmpegKit
+import com.arthenica.ffmpegkit.ReturnCode
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
@@ -90,10 +98,12 @@ import org.telegram.ui.Components.EditTextBoldCursor
import org.telegram.ui.Components.Forum.ForumUtilities
import org.telegram.ui.Components.LayoutHelper
import org.telegram.ui.Components.TranscribeButton
+import xyz.nextalone.gen.Config
import xyz.nextalone.nnngram.helpers.QrHelper
import xyz.nextalone.nnngram.helpers.QrHelper.readQr
import xyz.nextalone.nnngram.tryOrLog
import java.io.File
+import java.io.FileInputStream
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
@@ -417,7 +427,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) {
@@ -867,14 +877,84 @@ 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 = "-y -vcodec libvpx-vp9 -i '$path' -lavfi split[v],palettegen,[v]paletteuse '$outputPath'"
+ FFmpegKit.executeAsync(cmd) { session ->
+ val returnCode = session.returnCode
+ if (ReturnCode.isSuccess(returnCode)) {
+ MediaController.saveFile(outputPath, activity, 0, null, null, callback)
+ } else {
+ Log.e("FFmpegKit", "Failed to convert to GIF: $returnCode, file: $path")
+ Toast.makeText(activity, "Failed to convert to GIF, Use Mp4", Toast.LENGTH_SHORT).show()
+ 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 = "-i '${tempDir.absolutePath}/%d.png' -vf palettegen=stats_mode=diff -y '${tempDir.absolutePath}/palette.png'"
+ val createGifCommand = "-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'"
+ FFmpegKit.executeAsync(generatePaletteCommand) { session ->
+ var returnCode = session.returnCode
+ if (ReturnCode.isSuccess(returnCode)) {
+ FFmpegKit.executeAsync(createGifCommand) { session1 ->
+ returnCode = session1.returnCode
+ if (ReturnCode.isSuccess(returnCode)) {
+ MediaController.saveFile(outputPath, activity, 0, null, null, callback)
+ } else {
+ Log.e("FFmpegKit", "Failed to convert to GIF: $returnCode, file: $path")
+ Toast.makeText(activity, "Failed to convert to GIF, Use tgs", Toast.LENGTH_SHORT).show()
+ }
+ tempDir.deleteRecursively()
+ }
+ } else {
+ Log.e("FFmpegKit", "Failed to convert to GIF: $returnCode, file: $path")
+ Toast.makeText(activity, "Failed to convert to GIF, Use tgs", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
} else {
val image = BitmapFactory.decodeFile(path)
if (image != null) {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6bf555cc5d..f5036a7c19 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -22,6 +22,7 @@ kotlinxCoroutinesAndroid = "1.9.0"
kotlinxSerializationJson = "1.8.0"
ktor = "3.0.3"
languageId = "17.0.6"
+lottie = "6.4.1"
osmdroidAndroid = "6.1.20"
playServicesLocation = "21.3.0"
playServicesVision = "20.1.3"
@@ -32,6 +33,7 @@ processPhoenix = "3.0.0"
sharetarget = "1.2.0"
paletteKtx = "1.0.0"
rust = "0.9.4"
+smartExceptionJava = "0.2.1" # FFmpegKit requires it
[libraries]
checker-compat-qual = { module = "org.checkerframework:checker-compat-qual", version.ref = "checkerCompatQual" }
@@ -53,6 +55,7 @@ ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref =
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
language-id = { module = "com.google.mlkit:language-id", version.ref = "languageId" }
+lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" }
osmdroid-android = { module = "org.osmdroid:osmdroid-android", version.ref = "osmdroidAndroid" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
play-services-vision = { module = "com.google.android.gms:play-services-vision", version.ref = "playServicesVision" }
@@ -70,6 +73,7 @@ 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" }
+smart-exception-java = { module = "com.arthenica:smart-exception-java", version.ref = "smartExceptionJava" }
[plugins]
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }