diff --git a/TMessagesProj/build.gradle.kts b/TMessagesProj/build.gradle.kts index bb3f1f3b71..b949ef68a4 100644 --- a/TMessagesProj/build.gradle.kts +++ b/TMessagesProj/build.gradle.kts @@ -97,6 +97,9 @@ dependencies { implementation(libs.ktor.client.contentNegotiation) implementation(libs.ktor.serialization.json) + implementation(files("libs/ffmpeg-kit-video-4.4.LTS.aar")) + 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/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..116652527b 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" @@ -53,6 +54,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" }