commit 5498f8206dff28f865f8824ad3caf6c162531a54
parent 05a495d51ffa7ed63248d65dac9273c0f83ec83d
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sun, 21 Jan 2024 11:00:31 +0100

feat(core): long press to force download

Diffstat:
Mcommon/src/main/assets/lang/en_US.json | 8++++----
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/DownloaderConfig.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt | 16++++++----------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt | 48++++++++++++++++++++++++++++++------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt | 19+++++++++++++------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaContextActionMenu.kt | 25+++++++++++++++++--------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaDownloadIconMenu.kt | 10+++++++++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/AndroidExt.kt | 8++++++++
8 files changed, 88 insertions(+), 48 deletions(-)

diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -182,11 +182,11 @@ }, "opera_download_button": { "name": "Opera Download Button", - "description": "Adds a download button on the top right corner when viewing a Snap" + "description": "Adds a download button on the top right corner when viewing a Snap.\nLong press on buttons will force download" }, - "chat_download_context_menu": { - "name": "Chat Download Context Menu", - "description": "Allows you to download media from a conversation by long-pressing them" + "download_context_menu": { + "name": "Download Context Menu", + "description": "Allows you to download/preview messages from a conversation or a story using the context menu.\nLong press on buttons will force download" }, "ffmpeg_options": { "name": "FFmpeg Options", diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/DownloaderConfig.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/DownloaderConfig.kt @@ -43,7 +43,7 @@ class DownloaderConfig : ConfigContainer() { } val downloadProfilePictures = boolean("download_profile_pictures") { requireRestart() } val operaDownloadButton = boolean("opera_download_button") { requireRestart() } - val chatDownloadContextMenu = boolean("chat_download_context_menu") + val downloadContextMenu = boolean("download_context_menu") val ffmpegOptions = container("ffmpeg_options", FFMpegOptions()) { addNotices(FeatureNotice.UNSTABLE) } val logging = multiple("logging", "started", "success", "progress", "failure").apply { set(mutableListOf("success", "progress", "failure")) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -94,6 +94,7 @@ class SnapEnhance { val isMainActivityNotNull = appContext.mainActivity != null appContext.mainActivity = this if (isMainActivityNotNull || !appContext.mappings.isMappingsLoaded) return@hookMainActivity + appContext.isMainActivityPaused = false onActivityCreate() jetpackComposeResourceHook() appContext.actionManager.onNewIntent(intent) @@ -108,18 +109,13 @@ class SnapEnhance { appContext.actionManager.onNewIntent(param.argNullable(0)) } - var activityWasResumed = false - //we need to reload the config when the app is resumed - //FIXME: called twice at first launch hookMainActivity("onResume") { - appContext.isMainActivityPaused = false - if (!activityWasResumed) { - activityWasResumed = true - return@hookMainActivity + if (appContext.isMainActivityPaused.also { + appContext.isMainActivityPaused = false + }) { + appContext.reloadConfig() + syncRemote() } - - appContext.reloadConfig() - syncRemote() } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt @@ -75,10 +75,11 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp mediaAuthor: String, creationTimestamp: Long? = null, downloadSource: MediaDownloadSource, - friendInfo: FriendInfo? = null + friendInfo: FriendInfo? = null, + forceAllowDuplicate: Boolean = false ): DownloadManagerClient { val generatedHash = ( - if (!context.config.downloader.allowDuplicate.get()) mediaIdentifier + if (!context.config.downloader.allowDuplicate.get() && !forceAllowDuplicate) mediaIdentifier else UUID.randomUUID().toString() ).longHashCode().absoluteValue.toString(16) @@ -135,10 +136,10 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp /* * Download the last seen media */ - fun downloadLastOperaMediaAsync() { + fun downloadLastOperaMediaAsync(allowDuplicate: Boolean) { if (lastSeenMapParams == null || lastSeenMediaInfoMap == null) return context.executeAsync { - handleOperaMedia(lastSeenMapParams!!, lastSeenMediaInfoMap!!, true) + handleOperaMedia(lastSeenMapParams!!, lastSeenMediaInfoMap!!, true, allowDuplicate) } } @@ -162,9 +163,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp setNeutralButton("Copy") { _, _ -> this@MediaDownloader.context.copyToClipboard(mediaInfoText) } - setPositiveButton("Download") { _, _ -> - downloadLastOperaMediaAsync() - } setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() } }.show() } @@ -229,7 +227,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp private fun handleOperaMedia( paramMap: ParamMap, mediaInfoMap: Map<SplitMediaAssetType, MediaInfo>, - forceDownload: Boolean + forceDownload: Boolean, + forceAllowDuplicate: Boolean = false ) { //messages paramMap["MESSAGE_ID"]?.toString()?.takeIf { forceDownload || canAutoDownload("friend_snaps") }?.let { id -> @@ -257,7 +256,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp mediaAuthor = authorUsername, creationTimestamp = conversationMessage.creationTimestamp, downloadSource = MediaDownloadSource.CHAT_MEDIA, - friendInfo = author + friendInfo = author, + forceAllowDuplicate = forceAllowDuplicate ), mediaInfoMap) return @@ -301,7 +301,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp creationTimestamp = paramMap["PLAYABLE_STORY_SNAP_RECORD"]?.toString()?.substringAfter("timestamp=") ?.substringBefore(",")?.toLongOrNull(), downloadSource = MediaDownloadSource.STORY, - friendInfo = author + friendInfo = author, + forceAllowDuplicate = forceAllowDuplicate, ), mediaInfoMap) return } @@ -331,6 +332,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp mediaAuthor = author, downloadSource = MediaDownloadSource.PUBLIC_STORY, creationTimestamp = paramMap["SNAP_TIMESTAMP"]?.toString()?.toLongOrNull(), + forceAllowDuplicate = forceAllowDuplicate, ), mediaInfoMap) return } @@ -342,6 +344,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp downloadSource = MediaDownloadSource.SPOTLIGHT, mediaAuthor = paramMap["CREATOR_DISPLAY_NAME"].toString(), creationTimestamp = paramMap["SNAP_TIMESTAMP"]?.toString()?.toLongOrNull(), + forceAllowDuplicate = forceAllowDuplicate, ), mediaInfoMap) return } @@ -435,7 +438,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp provideDownloadManagerClient( mediaIdentifier = "${paramMap["STORY_ID"]}-${firstChapter.offset}-${lastChapter.offset}", downloadSource = MediaDownloadSource.PUBLIC_STORY, - mediaAuthor = storyName + mediaAuthor = storyName, + forceAllowDuplicate = forceAllowDuplicate, ).downloadDashMedia( playlistUrl, firstChapter.offset.plus(100), @@ -505,7 +509,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp friendInfo: FriendInfo, message: ConversationMessage, authorName: String, - attachments: List<DecodedAttachment> + attachments: List<DecodedAttachment>, + forceAllowDuplicate: Boolean = false ) { //TODO: stickers attachments.forEach { attachment -> @@ -514,7 +519,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp mediaIdentifier = "${message.clientConversationId}${message.senderId}${message.serverMessageId}${attachment.mediaUniqueId}", downloadSource = MediaDownloadSource.CHAT_MEDIA, mediaAuthor = authorName, - friendInfo = friendInfo + friendInfo = friendInfo, + forceAllowDuplicate = forceAllowDuplicate, ).downloadSingleMedia( mediaData = attachment.mediaUrlKey!!, mediaType = DownloadMediaType.PROTO_MEDIA, @@ -531,7 +537,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp @SuppressLint("SetTextI18n") @OptIn(ExperimentalCoroutinesApi::class) - fun downloadMessageId(messageId: Long, isPreview: Boolean = false) { + fun downloadMessageId(messageId: Long, forceAllowDuplicate: Boolean = false, isPreview: Boolean = false) { val messageLogger = context.feature(MessageLogger::class) val message = context.database.getConversationMessageFromId(messageId) ?: throw Exception("Message not found in database") @@ -570,7 +576,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp context.mainActivity == null // we can't show alert dialogs when it downloads from a notification, so it downloads the first one ) { downloadMessageAttachments(friendInfo, message, authorName, - listOf(decodedAttachments.first()) + listOf(decodedAttachments.first()), + forceAllowDuplicate = forceAllowDuplicate ) return } @@ -595,7 +602,9 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp setTitle(translations["select_attachments_title"]) setNegativeButton(this@MediaDownloader.context.translation["button.cancel"]) { dialog, _ -> dialog.dismiss() } setPositiveButton(this@MediaDownloader.context.translation["button.download"]) { _, _ -> - downloadMessageAttachments(friendInfo, message, authorName, selectedAttachments.map { decodedAttachments[it] }) + downloadMessageAttachments(friendInfo, message, authorName, selectedAttachments.map { decodedAttachments[it] }, + forceAllowDuplicate = forceAllowDuplicate + ) } }.show() } @@ -698,9 +707,12 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp /** * Called when a message is focused in chat */ - fun onMessageActionMenu(isPreviewMode: Boolean) { + fun onMessageActionMenu(isPreviewMode: Boolean, forceAllowDuplicate: Boolean = false) { val messaging = context.feature(Messaging::class) if (messaging.openedConversationUUID == null) return - downloadMessageId(messaging.lastFocusedMessageId, isPreviewMode) + + context.executeAsync { + downloadMessageId(messaging.lastFocusedMessageId, forceAllowDuplicate, isPreviewMode) + } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt @@ -33,6 +33,7 @@ import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getDimens +import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress import java.text.SimpleDateFormat import java.util.Date @@ -80,7 +81,7 @@ class ChatActionMenu : AbstractMenu() { override fun init() { runCatching { - if (!context.config.downloader.chatDownloadContextMenu.get() && context.config.messaging.messageLogger.globalState != true && !context.isDeveloper) return + if (!context.config.downloader.downloadContextMenu.get() && context.config.messaging.messageLogger.globalState != true && !context.isDeveloper) return context.androidContext.classLoader.loadClass("com.snap.messaging.chat.features.actionmenu.ActionMenuChatItemContainer") .hook("onMeasure", HookStage.BEFORE) { param -> param.setArg(1, @@ -130,12 +131,14 @@ class ChatActionMenu : AbstractMenu() { } } - if (context.config.downloader.chatDownloadContextMenu.get()) { + if (context.config.downloader.downloadContextMenu.get()) { + val mediaDownloader = context.feature(MediaDownloader::class) + injectButton(Button(viewGroup.context).apply { text = this@ChatActionMenu.context.translation["chat_action_menu.preview_button"] setOnClickListener { closeActionMenu() - this@ChatActionMenu.context.executeAsync { feature(MediaDownloader::class).onMessageActionMenu(true) } + mediaDownloader.onMessageActionMenu(true) } }) @@ -143,9 +146,13 @@ class ChatActionMenu : AbstractMenu() { text = this@ChatActionMenu.context.translation["chat_action_menu.download_button"] setOnClickListener { closeActionMenu() - this@ChatActionMenu.context.executeAsync { - feature(MediaDownloader::class).onMessageActionMenu(false) - } + mediaDownloader.onMessageActionMenu(false) + } + setOnLongClickListener { + closeActionMenu() + context.vibrateLongPress() + mediaDownloader.onMessageActionMenu(isPreviewMode = false, forceAllowDuplicate = true) + true } }) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaContextActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaContextActionMenu.kt @@ -13,6 +13,7 @@ import me.rhunk.snapenhance.core.ui.applyTheme import me.rhunk.snapenhance.core.ui.menu.AbstractMenu import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent import me.rhunk.snapenhance.core.util.ktx.getId +import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress import me.rhunk.snapenhance.core.wrapper.impl.ScSize import java.text.DateFormat import java.util.Date @@ -128,14 +129,22 @@ class OperaContextActionMenu : AbstractMenu() { } } - linearLayout.addView(Button(view.context).apply { - text = translation["download"] - setOnClickListener { - mediaDownloader.downloadLastOperaMediaAsync() - parentView.triggerCloseTouchEvent() - } - applyTheme(isAmoled = false) - }) + if (context.config.downloader.downloadContextMenu.get()) { + linearLayout.addView(Button(view.context).apply { + text = translation["download"] + setOnClickListener { + mediaDownloader.downloadLastOperaMediaAsync(allowDuplicate = false) + parentView.triggerCloseTouchEvent() + } + setOnLongClickListener { + context.vibrateLongPress() + mediaDownloader.downloadLastOperaMediaAsync(allowDuplicate = true) + parentView.triggerCloseTouchEvent() + true + } + applyTheme(isAmoled = false) + }) + } if (context.isDeveloper) { linearLayout.addView(Button(view.context).apply { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaDownloadIconMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaDownloadIconMenu.kt @@ -11,6 +11,7 @@ import me.rhunk.snapenhance.core.ui.children import me.rhunk.snapenhance.core.ui.menu.AbstractMenu import me.rhunk.snapenhance.core.util.ktx.getDimens import me.rhunk.snapenhance.core.util.ktx.getDrawable +import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress class OperaDownloadIconMenu : AbstractMenu() { private val downloadSvgDrawable by lazy { context.resources.getDrawable("svg_download", context.androidContext.theme) } @@ -21,6 +22,8 @@ class OperaDownloadIconMenu : AbstractMenu() { override fun inject(parent: ViewGroup, view: View, viewConsumer: (View) -> Unit) { if (!context.config.downloader.operaDownloadButton.get()) return + val mediaDownloader = context.feature(MediaDownloader::class) + parent.addView(ImageView(view.context).apply { setImageDrawable(downloadSvgDrawable) setColorFilter(Color.WHITE) @@ -33,7 +36,12 @@ class OperaDownloadIconMenu : AbstractMenu() { gravity = Gravity.TOP or Gravity.END } setOnClickListener { - this@OperaDownloadIconMenu.context.feature(MediaDownloader::class).downloadLastOperaMediaAsync() + mediaDownloader.downloadLastOperaMediaAsync(allowDuplicate = false) + } + setOnLongClickListener { + context.vibrateLongPress() + mediaDownloader.downloadLastOperaMediaAsync(allowDuplicate = true) + true } addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/AndroidExt.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/AndroidExt.kt @@ -1,10 +1,13 @@ package me.rhunk.snapenhance.core.util.ktx import android.annotation.SuppressLint +import android.content.Context import android.content.res.Resources import android.content.res.Resources.Theme import android.content.res.TypedArray import android.graphics.drawable.Drawable +import android.os.VibrationEffect +import android.os.Vibrator import me.rhunk.snapenhance.common.Constants @@ -31,3 +34,7 @@ fun Resources.getStyledAttributes(name: String, theme: Theme): TypedArray { fun Resources.getDrawable(name: String, theme: Theme): Drawable { return getDrawable(getIdentifier(name, "drawable"), theme) } + +fun Context.vibrateLongPress() { + getSystemService(Vibrator::class.java).vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE)) +}+ \ No newline at end of file