From 47e9a0e0b1b8f78e50feed8f1ae077324d48a2bb Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Wed, 20 Dec 2023 23:44:19 +0100 Subject: [PATCH 01/53] refactor: NativeUnaryCallEvent --- .../rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt | 2 +- core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt | 4 ++-- .../impl/{UnaryCallEvent.kt => NativeUnaryCallEvent.kt} | 2 +- .../core/features/impl/experiments/EndToEndEncryption.kt | 4 ++-- .../core/features/impl/messaging/PreventMessageSending.kt | 4 ++-- .../snapenhance/core/features/impl/messaging/SendOverride.kt | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) rename core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/{UnaryCallEvent.kt => NativeUnaryCallEvent.kt} (87%) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt index f6ebd2a1d..314904822 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt @@ -299,7 +299,7 @@ class HomeSection : Section() { .scale(1.75f) ) Text( - text = ("\u0065" + "\u0063" + "\u006e" + "\u0061" + "\u0068" + "\u006e" + "\u0045" + "\u0070" + "\u0061" + "\u006e" + "\u0053").reversed(), + text = arrayOf("\u0065", "\u0063", "\u006e", "\u0061", "\u0068", "\u006e", "\u0045", "\u0070", "\u0061", "\u006e", "\u0053").reversed().joinToString(""), fontSize = 30.sp, modifier = Modifier.padding(16.dp), ) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt index b1b107187..8e222f5aa 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -18,7 +18,7 @@ import me.rhunk.snapenhance.core.bridge.BridgeClient import me.rhunk.snapenhance.core.bridge.loadFromBridge import me.rhunk.snapenhance.core.data.SnapClassCache import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent -import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent +import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent import me.rhunk.snapenhance.core.util.LSPatchUpdater import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook @@ -165,7 +165,7 @@ class SnapEnhance { if (appContext.config.experimental.nativeHooks.globalState != true) return@apply initOnce(appContext.androidContext.classLoader) nativeUnaryCallCallback = { request -> - appContext.event.post(UnaryCallEvent(request.uri, request.buffer)) { + appContext.event.post(NativeUnaryCallEvent(request.uri, request.buffer)) { request.buffer = buffer request.canceled = canceled } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/UnaryCallEvent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/NativeUnaryCallEvent.kt similarity index 87% rename from core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/UnaryCallEvent.kt rename to core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/NativeUnaryCallEvent.kt index da39a1deb..5d08af391 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/UnaryCallEvent.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/NativeUnaryCallEvent.kt @@ -2,7 +2,7 @@ package me.rhunk.snapenhance.core.event.events.impl import me.rhunk.snapenhance.core.event.events.AbstractHookEvent -class UnaryCallEvent( +class NativeUnaryCallEvent( val uri: String, var buffer: ByteArray ) : AbstractHookEvent() \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt index 45b3be793..2766f1b28 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt @@ -22,7 +22,7 @@ import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent -import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent +import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.impl.messaging.Messaging @@ -417,7 +417,7 @@ class EndToEndEncryption : MessagingRuleFeature( } } - context.event.subscribe(UnaryCallEvent::class) { event -> + context.event.subscribe(NativeUnaryCallEvent::class) { event -> if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe val protoReader = ProtoReader(event.buffer) var hasStory = false diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/PreventMessageSending.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/PreventMessageSending.kt index e16286ca2..1d18d3d31 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/PreventMessageSending.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/PreventMessageSending.kt @@ -3,7 +3,7 @@ package me.rhunk.snapenhance.core.features.impl.messaging import me.rhunk.snapenhance.common.data.NotificationType import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent -import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent +import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage @@ -13,7 +13,7 @@ class PreventMessageSending : Feature("Prevent message sending", loadParams = Fe override fun asyncOnActivityCreate() { val preventMessageSending by context.config.messaging.preventMessageSending - context.event.subscribe(UnaryCallEvent::class, { preventMessageSending.contains("snap_replay") }) { event -> + context.event.subscribe(NativeUnaryCallEvent::class, { preventMessageSending.contains("snap_replay") }) { event -> if (event.uri != "/messagingcoreservice.MessagingCoreService/UpdateContentMessage") return@subscribe event.buffer = ProtoEditor(event.buffer).apply { edit(3) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt index aef2218df..b2fca896d 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt @@ -4,7 +4,7 @@ import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent -import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent +import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.messaging.MessageSender @@ -29,7 +29,7 @@ class SendOverride : Feature("Send Override", loadParams = FeatureLoadParams.INI } override fun init() { - context.event.subscribe(UnaryCallEvent::class) { event -> + context.event.subscribe(NativeUnaryCallEvent::class) { event -> if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe val protoEditor = ProtoEditor(event.buffer) From cc94ea93b27398a0ef17b1d6e259f525a2d796ae Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Wed, 20 Dec 2023 23:45:12 +0100 Subject: [PATCH 02/53] feat: suspend location updates --- common/src/main/assets/lang/en_US.json | 8 +++ .../common/bridge/types/BridgeFileType.kt | 3 +- .../snapenhance/common/config/impl/Global.kt | 1 + .../impl/global/SuspendLocationUpdates.kt | 56 +++++++++++++++++++ .../core/manager/impl/FeatureManager.kt | 1 + 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index f4387e557..9edbf7e5d 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -453,6 +453,10 @@ } } }, + "suspend_location_updates": { + "name": "Suspend Location Updates", + "description": "Adds a button in map settings to suspend location updates" + }, "snapchat_plus": { "name": "Snapchat Plus", "description": "Enables Snapchat Plus features\nSome Server-sided features may not work" @@ -1030,5 +1034,9 @@ "streaks_reminder": { "notification_title": "Streaks", "notification_text": "You will lose your Streak with {friend} in {hoursLeft} hours" + }, + + "suspend_location_updates": { + "switch_text": "Suspend Location Updates" } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/BridgeFileType.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/BridgeFileType.kt index d0ba4d67b..a117ec462 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/BridgeFileType.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/BridgeFileType.kt @@ -8,7 +8,8 @@ enum class BridgeFileType(val value: Int, val fileName: String, val displayName: CONFIG(0, "config.json", "Config"), MAPPINGS(1, "mappings.json", "Mappings"), MESSAGE_LOGGER_DATABASE(2, "message_logger.db", "Message Logger",true), - PINNED_CONVERSATIONS(3, "pinned_conversations.txt", "Pinned Conversations"); + PINNED_CONVERSATIONS(3, "pinned_conversations.txt", "Pinned Conversations"), + SUSPEND_LOCATION_STATE(4, "suspend_location_state.txt", "Suspend Location State"); fun resolve(context: Context): File = if (isDatabase) { context.getDatabasePath(fileName) diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt index 5a6ec3a71..c5f7143bd 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt @@ -8,6 +8,7 @@ class Global : ConfigContainer() { val coordinates = mapCoordinates("coordinates", 0.0 to 0.0) { requireRestart()} // lat, long } val spoofLocation = container("spoofLocation", SpoofLocation()) + val suspendLocationUpdates = boolean("suspend_location_updates") { requireRestart() } val snapchatPlus = boolean("snapchat_plus") { requireRestart() } val disableConfirmationDialogs = multiple("disable_confirmation_dialogs", "remove_friend", "block_friend", "ignore_friend", "hide_friend", "hide_conversation", "clear_conversation") { requireRestart() } val disableMetrics = boolean("disable_metrics") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt new file mode 100644 index 000000000..767a13e3c --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt @@ -0,0 +1,56 @@ +package me.rhunk.snapenhance.core.features.impl.global + +import android.view.ViewGroup +import android.widget.Switch +import me.rhunk.snapenhance.common.bridge.types.BridgeFileType +import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent +import me.rhunk.snapenhance.core.features.BridgeFileFeature +import me.rhunk.snapenhance.core.features.FeatureLoadParams +import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper +import me.rhunk.snapenhance.core.util.hook.HookStage +import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.core.util.ktx.getId + +//TODO: bridge shared preferences +class SuspendLocationUpdates : BridgeFileFeature( + "Suspend Location Updates", + loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC, + bridgeFileType = BridgeFileType.SUSPEND_LOCATION_STATE +) { + private val isEnabled get() = context.config.global.suspendLocationUpdates.get() + override fun init() { + if (!isEnabled) return + reload() + + context.classCache.unifiedGrpcService.hook("bidiStreamingCall", HookStage.BEFORE) { param -> + val uri = param.arg(0) + if (uri == "/snapchat.valis.Valis/Communicate" && exists("true")) { + param.setResult(null) + } + } + } + + override fun onActivityCreate() { + if (!isEnabled) return + + val locationSharingSettingsContainerId = context.resources.getId("location_sharing_settings_container") + val recyclerViewContainerId = context.resources.getId("recycler_view_container") + + context.event.subscribe(AddViewEvent::class) { event -> + if (event.parent.id == locationSharingSettingsContainerId && event.view.id == recyclerViewContainerId) { + (event.view as ViewGroup).addView(Switch(event.view.context).apply { + isChecked = exists("true") + ViewAppearanceHelper.applyTheme(this) + text = this@SuspendLocationUpdates.context.translation["suspend_location_updates.switch_text"] + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + setOnCheckedChangeListener { _, isChecked -> + setState("true", isChecked) + } + }) + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt index 716080d2e..97df0f868 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt @@ -115,6 +115,7 @@ class FeatureManager( FideliusIndicator::class, EditTextOverride::class, PreventForcedLogout::class, + SuspendLocationUpdates::class, ) initializeFeatures() From 0d3bffb05bc14af3dab21a05ad2795876e34dfc5 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 21 Dec 2023 00:03:39 +0100 Subject: [PATCH 03/53] feat: custom path format --- common/src/main/assets/lang/en_US.json | 4 ++ .../common/config/impl/DownloaderConfig.kt | 1 + .../common/data/download/DownloadRequest.kt | 47 +++++++++++-------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 9edbf7e5d..d417d1282 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -212,6 +212,10 @@ "logging": { "name": "Logging", "description": "Shows toasts when media is downloading" + }, + "custom_path_format": { + "name": "Custom Path Format", + "description": "Specify a custom path format for downloaded media\n\nAvailable variables:\n - %username%\n - %source%\n - %hash%\n - %date_time%" } } }, 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 index 1e4e7c52b..cfed49b40 100644 --- 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 @@ -48,4 +48,5 @@ class DownloaderConfig : ConfigContainer() { val logging = multiple("logging", "started", "success", "progress", "failure").apply { set(mutableListOf("success", "progress", "failure")) } + val customPathFormat = string("custom_path_format") { addNotices(FeatureNotice.UNSTABLE) } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/download/DownloadRequest.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/download/DownloadRequest.kt index 71f1f2329..9f40f904d 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/download/DownloadRequest.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/download/DownloadRequest.kt @@ -44,8 +44,8 @@ fun createNewFilePath( creationTimestamp: Long? ): String { val pathFormat by config.downloader.pathFormat + val customPathFormat by config.downloader.customPathFormat val sanitizedMediaAuthor = mediaAuthor.sanitizeForPath().ifEmpty { hexHash } - val currentDateTime = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ENGLISH).format(creationTimestamp ?: System.currentTimeMillis()) val finalPath = StringBuilder() @@ -58,26 +58,35 @@ fun createNewFilePath( } } - if (pathFormat.contains("create_author_folder")) { - finalPath.append(sanitizedMediaAuthor).append("/") - } - if (pathFormat.contains("create_source_folder")) { - finalPath.append(downloadSource.pathName).append("/") - } - if (pathFormat.contains("append_hash")) { - appendFileName(hexHash) - } - if (pathFormat.contains("append_source")) { - appendFileName(downloadSource.pathName) - } - if (pathFormat.contains("append_username")) { - appendFileName(sanitizedMediaAuthor) - } - if (pathFormat.contains("append_date_time")) { - appendFileName(currentDateTime) + if (customPathFormat.isNotEmpty()) { + finalPath.append(customPathFormat + .replace("%username%", sanitizedMediaAuthor) + .replace("%source%", downloadSource.pathName) + .replace("%hash%", hexHash) + .replace("%date_time%", currentDateTime) + ) + } else { + if (pathFormat.contains("create_author_folder")) { + finalPath.append(sanitizedMediaAuthor).append("/") + } + if (pathFormat.contains("create_source_folder")) { + finalPath.append(downloadSource.pathName).append("/") + } + if (pathFormat.contains("append_hash")) { + appendFileName(hexHash) + } + if (pathFormat.contains("append_source")) { + appendFileName(downloadSource.pathName) + } + if (pathFormat.contains("append_username")) { + appendFileName(sanitizedMediaAuthor) + } + if (pathFormat.contains("append_date_time")) { + appendFileName(currentDateTime) + } } - if (finalPath.isEmpty()) finalPath.append(hexHash) + if (finalPath.isEmpty() || finalPath.isBlank()) finalPath.append(hexHash) return finalPath.toString() } \ No newline at end of file From 699da4974300df5ea23c846469ec7a6f67be5679 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 21 Dec 2023 22:44:13 +0100 Subject: [PATCH 04/53] fix(core/media_downloader): missing attachments --- .../impl/downloader/MediaDownloader.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) 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 index a69c3fe23..fa7b1beaf 100644 --- 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 @@ -549,11 +549,26 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp val friendInfo: FriendInfo = context.database.getFriendInfo(message.senderId!!) ?: throw Exception("Friend not found in database") val authorName = friendInfo.usernameForSorting!! - val decodedAttachments = messageLogger.takeIf { it.isEnabled }?.getMessageObject(message.clientConversationId!!, message.clientMessageId.toLong())?.let { + val decodedAttachments = (messageLogger.takeIf { it.isEnabled }?.getMessageObject(message.clientConversationId!!, message.clientMessageId.toLong())?.let { MessageDecoder.decode(it.getAsJsonObject("mMessageContent")) } ?: MessageDecoder.decode( protoReader = ProtoReader(message.messageContent!!) - ) + )).toMutableList() + + context.feature(Messaging::class).conversationManager?.takeIf { + decodedAttachments.isEmpty() + }?.also { conversationManager -> + runBlocking { + suspendCoroutine { continuation -> + conversationManager.fetchMessage(message.clientConversationId!!, message.clientMessageId.toLong(), onSuccess = { message -> + decodedAttachments.addAll(MessageDecoder.decode(message.messageContent!!)) + continuation.resumeWith(Result.success(Unit)) + }, onError = { + continuation.resumeWith(Result.success(Unit)) + }) + } + } + } if (decodedAttachments.isEmpty()) { context.shortToast(translations["no_attachments_toast"]) From cac0ccffc7fddc886e8729705b3547f5e8c50df4 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sat, 23 Dec 2023 01:08:36 +0100 Subject: [PATCH 05/53] feat: unsaveable messages - fix(auto_save): prevent saving unsaveable messages --- common/src/main/assets/lang/en_US.json | 9 ++++ .../snapenhance/common/config/impl/Rules.kt | 3 +- .../common/data/MessagingCoreObjects.kt | 8 +++- .../core/features/impl/messaging/AutoSave.kt | 2 +- .../features/impl/messaging/SendOverride.kt | 9 ++-- .../impl/tweaks/UnsaveableMessages.kt | 45 +++++++++++++++++++ .../core/manager/impl/FeatureManager.kt | 2 + .../core/wrapper/impl/MessageMetadata.kt | 2 + 8 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/UnsaveableMessages.kt diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index d417d1282..b3bbbceaf 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -97,6 +97,14 @@ "whitelist": "Auto save" } }, + "unsaveable_messages": { + "name": "Unsaveable Messages", + "description": "Prevents messages from being saved in chat by other people", + "options": { + "blacklist": "Exclude from Unsaveable Messages", + "whitelist": "Unsaveable Messages" + } + }, "hide_friend_feed": { "name": "Hide from Friend Feed" }, @@ -719,6 +727,7 @@ "friend_feed_menu_buttons": { "auto_download": "\u2B07\uFE0F Auto Download", "auto_save": "\uD83D\uDCAC Auto Save Messages", + "unsaveable_messages": "\u2B07\uFE0F Unsaveable Messages", "stealth": "\uD83D\uDC7B Stealth Mode", "mark_snaps_as_seen": "\uD83D\uDC40 Mark Snaps as seen", "mark_stories_as_seen_locally": "\uD83D\uDC40 Mark Stories as seen locally", diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Rules.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Rules.kt index 9d96e915b..18cf3553c 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Rules.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Rules.kt @@ -18,8 +18,9 @@ class Rules : ConfigContainer() { rules[ruleType] = unique(ruleType.key,"whitelist", "blacklist") { customTranslationPath = "rules.properties.${ruleType.key}" customOptionTranslationPath = "rules.modes" + addNotices(*ruleType.configNotices) }.apply { - set("whitelist") + set(ruleType.defaultValue) } } } diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/MessagingCoreObjects.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/MessagingCoreObjects.kt index a8ee2d468..fbd6d2f85 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/MessagingCoreObjects.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/MessagingCoreObjects.kt @@ -1,5 +1,6 @@ package me.rhunk.snapenhance.common.data +import me.rhunk.snapenhance.common.config.FeatureNotice import me.rhunk.snapenhance.common.util.SerializableDataObject @@ -29,11 +30,14 @@ enum class SocialScope( enum class MessagingRuleType( val key: String, val listMode: Boolean, - val showInFriendMenu: Boolean = true + val showInFriendMenu: Boolean = true, + val defaultValue: String? = "whitelist", + val configNotices: Array = emptyArray() ) { STEALTH("stealth", true), AUTO_DOWNLOAD("auto_download", true), - AUTO_SAVE("auto_save", true), + AUTO_SAVE("auto_save", true, defaultValue = "blacklist"), + UNSAVEABLE_MESSAGES("unsaveable_messages", true, configNotices = arrayOf(FeatureNotice.REQUIRE_NATIVE_HOOKS), defaultValue = null), HIDE_FRIEND_FEED("hide_friend_feed", false, showInFriendMenu = false), E2E_ENCRYPTION("e2e_encryption", false), PIN_CONVERSATION("pin_conversation", false, showInFriendMenu = false); diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt index f09c2f1af..00245c1ea 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt @@ -47,7 +47,7 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, } fun canSaveMessage(message: Message, headless: Boolean = false): Boolean { - if (message.messageState != MessageState.COMMITTED) return false + if (message.messageState != MessageState.COMMITTED || message.messageMetadata?.isSaveable != true) return false if (!headless && (context.mainActivity == null || context.isMainActivityPaused)) return false if (message.messageMetadata!!.savedBy!!.any { uuid -> uuid.toString() == context.database.myUserId }) return false diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt index b2fca896d..7a1aeaef9 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt @@ -3,8 +3,8 @@ package me.rhunk.snapenhance.core.features.impl.messaging import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.common.util.protobuf.ProtoReader -import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent +import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.messaging.MessageSender @@ -40,11 +40,8 @@ class SendOverride : Feature("Send Override", loadParams = FeatureLoadParams.INI } //make snaps savable in chat protoEditor.edit(4) { - val savableState = firstOrNull(7)?.value ?: return@edit - if (savableState == 2L) { - remove(7) - addVarInt(7, 3) - } + remove(7) + addVarInt(7, 3) } } event.buffer = protoEditor.toByteArray() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/UnsaveableMessages.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/UnsaveableMessages.kt new file mode 100644 index 000000000..41bb55847 --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/UnsaveableMessages.kt @@ -0,0 +1,45 @@ +package me.rhunk.snapenhance.core.features.impl.tweaks + +import me.rhunk.snapenhance.common.data.ContentType +import me.rhunk.snapenhance.common.data.MessagingRuleType +import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor +import me.rhunk.snapenhance.common.util.protobuf.ProtoReader +import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent +import me.rhunk.snapenhance.core.features.FeatureLoadParams +import me.rhunk.snapenhance.core.features.MessagingRuleFeature +import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID + +class UnsaveableMessages : MessagingRuleFeature( + "Unsaveable Messages", + MessagingRuleType.UNSAVEABLE_MESSAGES, + loadParams = FeatureLoadParams.INIT_SYNC +) { + override fun init() { + if (context.config.rules.getRuleState(MessagingRuleType.UNSAVEABLE_MESSAGES) == null) return + + context.event.subscribe(NativeUnaryCallEvent::class) { event -> + if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe + + val protoReader = ProtoReader(event.buffer) + val conversationIds = mutableListOf() + + protoReader.eachBuffer(3) { + if (contains(2)) { + return@eachBuffer + } + conversationIds.add(SnapUUID.fromBytes(getByteArray(1, 1, 1) ?: return@eachBuffer).toString()) + } + + if (conversationIds.all { canUseRule(it) }) { + event.buffer = ProtoEditor(event.buffer).apply { + edit(4) { + if ((firstOrNull(7)?.value ?: return@edit) == 2L && firstOrNull(2)?.value != ContentType.SNAP.id.toLong()) { + remove(7) + addVarInt(7, 3) + } + } + }.toByteArray() + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt index 97df0f868..d63c8caf3 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt @@ -21,6 +21,7 @@ import me.rhunk.snapenhance.core.features.impl.spying.StealthMode import me.rhunk.snapenhance.core.features.impl.tweaks.BypassScreenshotDetection import me.rhunk.snapenhance.core.features.impl.tweaks.CameraTweaks import me.rhunk.snapenhance.core.features.impl.tweaks.PreventMessageListAutoScroll +import me.rhunk.snapenhance.core.features.impl.tweaks.UnsaveableMessages import me.rhunk.snapenhance.core.features.impl.ui.* import me.rhunk.snapenhance.core.logger.CoreLogger import me.rhunk.snapenhance.core.manager.Manager @@ -80,6 +81,7 @@ class FeatureManager( AutoSave::class, UITweaks::class, ConfigurationOverride::class, + UnsaveableMessages::class, SendOverride::class, UnlimitedSnapViewTime::class, BypassVideoLengthRestriction::class, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageMetadata.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageMetadata.kt index 443b75587..6c53f0539 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageMetadata.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageMetadata.kt @@ -23,4 +23,6 @@ class MessageMetadata(obj: Any?) : AbstractWrapper(obj){ var reactions by field("mReactions") { (it as ArrayList<*>).map { i -> UserIdToReaction(i) }.toMutableList() } + @get:JSGetter @set:JSSetter + var isSaveable by field("mIsSaveable") } \ No newline at end of file From 7d6978f9618283715a9341a748e8dc57e21aead7 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 24 Dec 2023 17:17:45 +0100 Subject: [PATCH 06/53] feat(common/util): protobuf utils --- .../common/util/protobuf/GrpcReader.kt | 53 +++++++++++++++++++ .../common/util/protobuf/GrpcWriter.kt | 43 +++++++++++++++ .../core/database/DatabaseAccess.kt | 25 +++++++++ 3 files changed, 121 insertions(+) create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/GrpcReader.kt create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/GrpcWriter.kt diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/GrpcReader.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/GrpcReader.kt new file mode 100644 index 000000000..334b6c547 --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/GrpcReader.kt @@ -0,0 +1,53 @@ +package me.rhunk.snapenhance.common.util.protobuf + +class GrpcReader( + private val buffer: ByteArray +) { + private val _messages = mutableListOf() + private val _headers = mutableMapOf() + + val headers get() = _headers.toMap() + val messages get() = _messages.toList() + + fun read(reader: ProtoReader.() -> Unit) { + messages.forEach { message -> + message.reader() + } + } + + private var position: Int = 0 + + init { + read() + } + + private fun readByte() = buffer[position++].toInt() + + private fun readUInt32() = (readByte() and 0xFF) shl 24 or + ((readByte() and 0xFF) shl 16) or + ((readByte() and 0xFF) shl 8) or + (readByte() and 0xFF) + + private fun read() { + while (position < buffer.size) { + when (val type = readByte() and 0xFF) { + 0 -> { + val length = readUInt32() + val value = buffer.copyOfRange(position, position + length) + position += length + _messages.add(ProtoReader(value)) + } + 128 -> { + val length = readUInt32() + val rawHeaders = String(buffer.copyOfRange(position, position + length), Charsets.UTF_8) + position += length + rawHeaders.trim().split("\n").forEach { header -> + val (key, value) = header.split(":") + _headers[key] = value + } + } + else -> throw Exception("Unknown type $type") + } + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/GrpcWriter.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/GrpcWriter.kt new file mode 100644 index 000000000..dae137e3d --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/GrpcWriter.kt @@ -0,0 +1,43 @@ +package me.rhunk.snapenhance.common.util.protobuf + +import java.io.ByteArrayOutputStream + +fun ProtoWriter.toGrpcWriter() = GrpcWriter(toByteArray()) + +class GrpcWriter( + vararg val messages: ByteArray +) { + private val headers = mutableMapOf() + + fun addHeader(key: String, value: String) { + headers[key] = value + } + + fun toByteArray(): ByteArray { + val stream = ByteArrayOutputStream() + + fun writeByte(value: Int) = stream.write(value) + fun writeUInt(value: Int) { + writeByte(value ushr 24) + writeByte(value ushr 16) + writeByte(value ushr 8) + writeByte(value) + } + + messages.forEach { message -> + writeByte(0) + writeUInt(message.size) + stream.write(message) + } + + if (headers.isNotEmpty()){ + val rawHeaders = headers.map { (key, value) -> "$key:$value" }.joinToString("\n") + val rawHeadersBytes = rawHeaders.toByteArray(Charsets.UTF_8) + writeByte(-128) + writeUInt(rawHeadersBytes.size) + stream.write(rawHeadersBytes) + } + + return stream.toByteArray() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt index 7fc4c38f5..625c987ce 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt @@ -10,9 +10,11 @@ import me.rhunk.snapenhance.common.database.impl.FriendFeedEntry import me.rhunk.snapenhance.common.database.impl.FriendInfo import me.rhunk.snapenhance.common.database.impl.StoryEntry import me.rhunk.snapenhance.common.database.impl.UserConversationLink +import me.rhunk.snapenhance.common.util.ktx.getBlobOrNull import me.rhunk.snapenhance.common.util.ktx.getIntOrNull import me.rhunk.snapenhance.common.util.ktx.getInteger import me.rhunk.snapenhance.common.util.ktx.getStringOrNull +import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.manager.Manager @@ -360,4 +362,27 @@ class DatabaseAccess( close() } } + + fun getAccessTokens(userId: String): Map? { + return mainDb?.performOperation { + rawQuery( + "SELECT accessTokensPb FROM SnapToken WHERE userId = ?", + arrayOf(userId) + ).use { + if (!it.moveToFirst()) { + return@performOperation null + } + val reader = ProtoReader(it.getBlobOrNull("accessTokensPb") ?: return@performOperation null) + val services = mutableMapOf() + + reader.eachBuffer(1) { + val token = getString(1) ?: return@eachBuffer + val service = getString(2)?.substringAfterLast("/") ?: return@eachBuffer + services[service] = token + } + + services + } + } + } } \ No newline at end of file From 392cd95dacaced5aa1c297fb06d34545f6c8c4c6 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 24 Dec 2023 17:29:19 +0100 Subject: [PATCH 07/53] feat(scripting): module system --- .../me/rhunk/snapenhance/RemoteSideContext.kt | 2 +- .../snapenhance/messaging/ModDatabase.kt | 2 +- .../scripting/RemoteScriptManager.kt | 38 +++++---- .../{RemoteManagerIPC.kt => ManagerIPC.kt} | 16 ++-- ...ScriptConfig.kt => ManagerScriptConfig.kt} | 18 ++--- .../scripting/impl/ui/InterfaceManager.kt | 26 +++--- .../sections/scripting/ScriptsSection.kt | 43 ++++++++-- .../snapenhance/common/logger/LogChannel.kt | 2 + .../snapenhance/common/scripting/JSModule.kt | 81 ++++++++++++++++--- .../common/scripting/ScriptRuntime.kt | 29 +++---- .../common/scripting/ScriptingLogger.kt | 40 +++++++++ .../scripting/bindings/AbstractBinding.kt | 14 ++++ .../common/scripting/bindings/BindingSide.kt | 15 ++++ .../scripting/bindings/BindingsContext.kt | 9 +++ .../common/scripting/impl/ConfigInterface.kt | 9 ++- .../common/scripting/impl/IPCInterface.kt | 12 ++- .../common/scripting/type/ModuleInfo.kt | 2 +- .../me/rhunk/snapenhance/core/SnapEnhance.kt | 6 +- .../core/scripting/CoreScriptRuntime.kt | 15 ++-- .../core/scripting/impl/CoreIPC.kt | 17 ++-- .../core/scripting/impl/CoreScriptConfig.kt | 19 ++--- .../{ScriptHooker.kt => CoreScriptHooker.kt} | 29 ++++--- 22 files changed, 315 insertions(+), 129 deletions(-) rename app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/{RemoteManagerIPC.kt => ManagerIPC.kt} (74%) rename app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/{RemoteScriptConfig.kt => ManagerScriptConfig.kt} (75%) create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptingLogger.kt create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/AbstractBinding.kt create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingSide.kt create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingsContext.kt rename core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/{ScriptHooker.kt => CoreScriptHooker.kt} (88%) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt index 931dfac18..1f3796319 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -118,7 +118,7 @@ class RemoteSideContext( } scriptManager.runtime.eachModule { - callFunction("module.onManagerLoad", androidContext) + callFunction("module.onSnapEnhanceLoad", androidContext) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt index 85ed56cc0..2a04a68f0 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt @@ -268,7 +268,7 @@ class ModDatabase( version = cursor.getStringOrNull("version")!!, description = cursor.getStringOrNull("description"), author = cursor.getStringOrNull("author"), - grantPermissions = null + grantedPermissions = emptyList() ) ) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt index ac3a027d6..83a592d94 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt @@ -11,8 +11,8 @@ import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface import me.rhunk.snapenhance.common.scripting.impl.ConfigTransactionType import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.scripting.impl.IPCListeners -import me.rhunk.snapenhance.scripting.impl.RemoteManagerIPC -import me.rhunk.snapenhance.scripting.impl.RemoteScriptConfig +import me.rhunk.snapenhance.scripting.impl.ManagerIPC +import me.rhunk.snapenhance.scripting.impl.ManagerScriptConfig import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager import java.io.File import java.io.InputStream @@ -21,7 +21,9 @@ import kotlin.system.exitProcess class RemoteScriptManager( val context: RemoteSideContext, ) : IScripting.Stub() { - val runtime = ScriptRuntime(context.androidContext, context.log) + val runtime = ScriptRuntime(context.androidContext, context.log).apply { + scripting = this@RemoteScriptManager + } private var autoReloadListener: AutoReloadListener? = null private val autoReloadHandler by lazy { @@ -61,11 +63,11 @@ class RemoteScriptManager( fun init() { runtime.buildModuleObject = { module -> - module.extras["ipc"] = RemoteManagerIPC(module.moduleInfo, context.log, ipcListeners) - module.extras["im"] = InterfaceManager(module.moduleInfo, context.log) - module.extras["config"] = RemoteScriptConfig(this@RemoteScriptManager, module.moduleInfo, context.log).also { - it.load() - } + module.registerBindings( + ManagerIPC(ipcListeners), + InterfaceManager(), + ManagerScriptConfig(this@RemoteScriptManager) + ) } sync() @@ -74,12 +76,20 @@ class RemoteScriptManager( } } - fun loadScript(name: String) { - val content = getScriptContent(name) ?: return + fun getModulePath(name: String): String? { + return cachedModuleInfo.entries.find { it.value.name == name }?.key + } + + fun loadScript(path: String) { + val content = getScriptContent(path) ?: return if (context.config.root.scripting.autoReload.getNullable() != null) { - autoReloadHandler.addFile(getScriptsFolder()?.findFile(name) ?: return) + autoReloadHandler.addFile(getScriptsFolder()?.findFile(path) ?: return) } - runtime.load(name, content) + runtime.load(path, content) + } + + fun unloadScript(scriptPath: String) { + runtime.unload(scriptPath) } private fun getScriptInputStream(name: String, callback: (InputStream?) -> R): R { @@ -140,7 +150,7 @@ class RemoteScriptManager( value: String?, save: Boolean ): String? { - val scriptConfig = runtime.getModuleByName(module ?: return null)?.extras?.get("config") as? ConfigInterface ?: return null.also { + val scriptConfig = runtime.getModuleByName(module ?: return null)?.getBinding(ConfigInterface::class) ?: return null.also { context.log.warn("Failed to get config interface for $module") } val transactionType = ConfigTransactionType.fromKey(action) @@ -154,7 +164,7 @@ class RemoteScriptManager( ConfigTransactionType.SET -> set(key ?: return@runCatching null, value, save) ConfigTransactionType.SAVE -> save() ConfigTransactionType.LOAD -> load() - ConfigTransactionType.DELETE -> delete() + ConfigTransactionType.DELETE -> deleteConfig() else -> {} } null diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteManagerIPC.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerIPC.kt similarity index 74% rename from app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteManagerIPC.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerIPC.kt index 32860f1b3..57f5902c3 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteManagerIPC.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerIPC.kt @@ -2,17 +2,13 @@ package me.rhunk.snapenhance.scripting.impl import android.os.DeadObjectException import me.rhunk.snapenhance.bridge.scripting.IPCListener -import me.rhunk.snapenhance.common.logger.AbstractLogger import me.rhunk.snapenhance.common.scripting.impl.IPCInterface import me.rhunk.snapenhance.common.scripting.impl.Listener -import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import java.util.concurrent.ConcurrentHashMap typealias IPCListeners = ConcurrentHashMap>> // channel, eventName -> listeners -class RemoteManagerIPC( - private val moduleInfo: ModuleInfo, - private val logger: AbstractLogger, +class ManagerIPC( private val ipcListeners: IPCListeners = ConcurrentHashMap(), ) : IPCInterface() { companion object { @@ -20,22 +16,22 @@ class RemoteManagerIPC( } override fun on(eventName: String, listener: Listener) { - onBroadcast(moduleInfo.name, eventName, listener) + onBroadcast(context.moduleInfo.name, eventName, listener) } override fun emit(eventName: String, vararg args: String?) { - emit(moduleInfo.name, eventName, *args) + emit(context.moduleInfo.name, eventName, *args) } override fun onBroadcast(channel: String, eventName: String, listener: Listener) { ipcListeners.getOrPut(channel) { mutableMapOf() }.getOrPut(eventName) { mutableSetOf() }.add(object: IPCListener.Stub() { override fun onMessage(args: Array) { try { - listener(args) + listener(args.toList()) } catch (doe: DeadObjectException) { ipcListeners[channel]?.get(eventName)?.remove(this) } catch (t: Throwable) { - logger.error("Failed to receive message for channel: $channel, event: $eventName", t, TAG) + context.runtime.logger.error("Failed to receive message for channel: $channel, event: $eventName", t, TAG) } } }) @@ -48,7 +44,7 @@ class RemoteManagerIPC( } catch (doe: DeadObjectException) { ipcListeners[channel]?.get(eventName)?.remove(it) } catch (t: Throwable) { - logger.error("Failed to send message for channel: $channel, event: $eventName", t, TAG) + context.runtime.logger.error("Failed to send message for channel: $channel, event: $eventName", t, TAG) } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteScriptConfig.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerScriptConfig.kt similarity index 75% rename from app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteScriptConfig.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerScriptConfig.kt index a9776585e..552f67098 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/RemoteScriptConfig.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ManagerScriptConfig.kt @@ -1,18 +1,14 @@ package me.rhunk.snapenhance.scripting.impl import com.google.gson.JsonObject -import me.rhunk.snapenhance.common.logger.AbstractLogger import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface -import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.scripting.RemoteScriptManager import java.io.File -class RemoteScriptConfig( - private val remoteScriptManager: RemoteScriptManager, - moduleInfo: ModuleInfo, - private val logger: AbstractLogger, +class ManagerScriptConfig( + private val remoteScriptManager: RemoteScriptManager ) : ConfigInterface() { - private val configFile = File(remoteScriptManager.getModuleDataFolder(moduleInfo.name), "config.json") + private val configFile by lazy { File(remoteScriptManager.getModuleDataFolder(context.moduleInfo.name), "config.json") } private var config = JsonObject() override fun get(key: String, defaultValue: Any?): String? { @@ -46,12 +42,16 @@ class RemoteScriptConfig( } config = remoteScriptManager.context.gson.fromJson(configFile.readText(), JsonObject::class.java) }.onFailure { - logger.error("Failed to load config file", it) + context.runtime.logger.error("Failed to load config file", it) save() } } - override fun delete() { + override fun deleteConfig() { configFile.delete() } + + override fun onInit() { + load() + } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/InterfaceManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/InterfaceManager.kt index e4c2e4fa5..fbfc9c60a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/InterfaceManager.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/InterfaceManager.kt @@ -1,13 +1,13 @@ package me.rhunk.snapenhance.scripting.impl.ui -import me.rhunk.snapenhance.common.logger.AbstractLogger -import me.rhunk.snapenhance.common.scripting.type.ModuleInfo +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide +import me.rhunk.snapenhance.common.scripting.ktx.contextScope import me.rhunk.snapenhance.scripting.impl.ui.components.Node import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionNode import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionType import me.rhunk.snapenhance.scripting.impl.ui.components.impl.RowColumnNode -import org.mozilla.javascript.Context import org.mozilla.javascript.Function import org.mozilla.javascript.annotations.JSFunction @@ -73,27 +73,31 @@ class InterfaceBuilder { -class InterfaceManager( - private val moduleInfo: ModuleInfo, - private val logger: AbstractLogger -) { +class InterfaceManager : AbstractBinding("interface-manager", BindingSide.MANAGER) { private val interfaces = mutableMapOf InterfaceBuilder?>() fun buildInterface(name: String): InterfaceBuilder? { return interfaces[name]?.invoke() } + override fun onDispose() { + interfaces.clear() + } + + @Suppress("unused") @JSFunction fun create(name: String, callback: Function) { interfaces[name] = { val interfaceBuilder = InterfaceBuilder() runCatching { - Context.enter() - callback.call(Context.getCurrentContext(), callback, callback, arrayOf(interfaceBuilder)) - Context.exit() + contextScope { + callback.call(this, callback, callback, arrayOf(interfaceBuilder)) + } interfaceBuilder }.onFailure { - logger.error("Failed to create interface $name for ${moduleInfo.name}", it) + context.runtime.logger.error("Failed to create interface $name for ${context.moduleInfo.name}", it) }.getOrNull() } } + + override fun getObject() = this } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt index d8ae0602c..28f99f0c1 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.material.icons.filled.LibraryBooks import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* @@ -14,6 +15,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -70,12 +72,27 @@ class ScriptsSection : Section() { } Switch( checked = enabled, - onCheckedChange = { - context.modDatabase.setScriptEnabled(script.name, it) - if (it) { - context.scriptManager.loadScript(script.name) + onCheckedChange = { isChecked -> + context.modDatabase.setScriptEnabled(script.name, isChecked) + enabled = isChecked + runCatching { + val modulePath = context.scriptManager.getModulePath(script.name)!! + context.scriptManager.unloadScript(modulePath) + if (isChecked) { + context.scriptManager.loadScript(modulePath) + context.scriptManager.runtime.getModuleByName(script.name) + ?.callFunction("module.onSnapEnhanceLoad") + context.shortToast("Loaded script ${script.name}") + } else { + context.shortToast("Unloaded script ${script.name}") + } + }.onFailure { throwable -> + enabled = !isChecked + ("Failed to ${if (isChecked) "enable" else "disable"} script").let { + context.log.error(it, throwable) + context.shortToast(it) + } } - enabled = it } ) } @@ -130,7 +147,7 @@ class ScriptsSection : Section() { val settingsInterface = remember { val module = context.scriptManager.runtime.getModuleByName(script.name) ?: return@remember null runCatching { - (module.extras["im"] as? InterfaceManager)?.buildInterface("settings") + (module.getBinding(InterfaceManager::class))?.buildInterface("settings") }.onFailure { settingsError = it }.getOrNull() @@ -228,4 +245,18 @@ class ScriptsSection : Section() { ) } } + + @Composable + override fun TopBarActions(rowScope: RowScope) { + rowScope.apply { + IconButton(onClick = { + context.androidContext.startActivity(Intent(Intent.ACTION_VIEW).apply { + data = "https://github.com/SnapEnhance/docs".toUri() + flags = Intent.FLAG_ACTIVITY_NEW_TASK + }) + }) { + Icon(imageVector = Icons.Default.LibraryBooks, contentDescription = "Documentation") + } + } + } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/logger/LogChannel.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/logger/LogChannel.kt index 1d3024bd3..0d9696e5a 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/logger/LogChannel.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/logger/LogChannel.kt @@ -5,6 +5,8 @@ enum class LogChannel( val shortName: String ) { CORE("SnapEnhanceCore", "core"), + COMMON("SnapEnhanceCommon", "common"), + SCRIPTING("Scripting", "scripting"), NATIVE("SnapEnhanceNative", "native"), MANAGER("SnapEnhanceManager", "manager"), XPOSED("LSPosed-Bridge", "xposed"); diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt index 161da228a..e61cb3c38 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt @@ -2,6 +2,8 @@ package me.rhunk.snapenhance.common.scripting import android.os.Handler import android.widget.Toast +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingsContext import me.rhunk.snapenhance.common.scripting.ktx.contextScope import me.rhunk.snapenhance.common.scripting.ktx.putFunction import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject @@ -12,15 +14,23 @@ import org.mozilla.javascript.ScriptableObject import org.mozilla.javascript.Undefined import org.mozilla.javascript.Wrapper import java.lang.reflect.Modifier +import kotlin.reflect.KClass class JSModule( val scriptRuntime: ScriptRuntime, val moduleInfo: ModuleInfo, val content: String, ) { - val extras = mutableMapOf() + private val moduleBindings = mutableMapOf() private lateinit var moduleObject: ScriptableObject + private val moduleBindingContext by lazy { + BindingsContext( + moduleInfo = moduleInfo, + runtime = scriptRuntime + ) + } + fun load(block: ScriptableObject.() -> Unit) { contextScope { val classLoader = scriptRuntime.androidContext.classLoader @@ -33,7 +43,7 @@ class JSModule( putConst("author", this, moduleInfo.author) putConst("minSnapchatVersion", this, moduleInfo.minSnapchatVersion) putConst("minSEVersion", this, moduleInfo.minSEVersion) - putConst("grantPermissions", this, moduleInfo.grantPermissions) + putConst("grantedPermissions", this, moduleInfo.grantedPermissions) }) }) @@ -62,12 +72,16 @@ class JSModule( moduleObject.putFunction("findClass") { val className = it?.get(0).toString() - classLoader.loadClass(className) + runCatching { + classLoader.loadClass(className) + }.onFailure { throwable -> + scriptRuntime.logger.error("Failed to load class $className", throwable) + }.getOrNull() } moduleObject.putFunction("type") { args -> val className = args?.get(0).toString() - val clazz = classLoader.loadClass(className) + val clazz = runCatching { classLoader.loadClass(className) }.getOrNull() ?: return@putFunction Undefined.instance scriptableObject("JavaClassWrapper") { putFunction("newInstance") newInstance@{ args -> @@ -95,12 +109,12 @@ class JSModule( } moduleObject.putFunction("logInfo") { args -> - scriptRuntime.logger.info(args?.joinToString(" ") { - when (it) { - is Wrapper -> it.unwrap().toString() - else -> it.toString() - } - } ?: "null") + scriptRuntime.logger.info(argsToString(args)) + Undefined.instance + } + + moduleObject.putFunction("logError") { args -> + scriptRuntime.logger.error(argsToString(arrayOf(args?.get(0))), args?.get(1) as? Throwable ?: Throwable()) Undefined.instance } @@ -116,16 +130,38 @@ class JSModule( Undefined.instance } } + block(moduleObject) - extras.forEach { (key, value) -> - moduleObject.putConst(key, moduleObject, value) + + moduleBindings.forEach { (_, instance) -> + instance.context = moduleBindingContext + + runCatching { + instance.onInit() + }.onFailure { + scriptRuntime.logger.error("Failed to init binding ${instance.name}", it) + } } + + moduleObject.putFunction("require") { args -> + val bindingName = args?.get(0).toString() + moduleBindings[bindingName]?.getObject() + } + evaluateString(moduleObject, content, moduleInfo.name, 1, null) } } fun unload() { callFunction("module.onUnload") + moduleBindings.entries.removeIf { (name, binding) -> + runCatching { + binding.onDispose() + }.onFailure { + scriptRuntime.logger.error("Failed to dispose binding $name", it) + } + true + } } fun callFunction(name: String, vararg args: Any?) { @@ -143,4 +179,25 @@ class JSModule( } } } + fun registerBindings(vararg bindings: AbstractBinding) { + bindings.forEach { + moduleBindings[it.name] = it.apply { + context = moduleBindingContext + } + } + } + + @Suppress("UNCHECKED_CAST") + fun getBinding(clazz: KClass): T? { + return moduleBindings.values.find { clazz.isInstance(it) } as? T + } + + private fun argsToString(args: Array?): String { + return args?.joinToString(" ") { + when (it) { + is Wrapper -> it.unwrap().toString() + else -> it.toString() + } + } ?: "null" + } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptRuntime.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptRuntime.kt index 9c2678ba4..b9bed87db 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptRuntime.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptRuntime.kt @@ -1,6 +1,7 @@ package me.rhunk.snapenhance.common.scripting import android.content.Context +import me.rhunk.snapenhance.bridge.scripting.IScripting import me.rhunk.snapenhance.common.logger.AbstractLogger import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import org.mozilla.javascript.ScriptableObject @@ -10,9 +11,13 @@ import java.io.InputStream open class ScriptRuntime( val androidContext: Context, - val logger: AbstractLogger, + logger: AbstractLogger, ) { + val logger = ScriptingLogger(logger) + + lateinit var scripting: IScripting var buildModuleObject: ScriptableObject.(JSModule) -> Unit = {} + private val modules = mutableMapOf() fun eachModule(f: JSModule.() -> Unit) { @@ -55,7 +60,7 @@ open class ScriptRuntime( author = properties["author"], minSnapchatVersion = properties["minSnapchatVersion"]?.toLong(), minSEVersion = properties["minSEVersion"]?.toLong(), - grantPermissions = properties["permissions"]?.split(",")?.map { it.trim() }, + grantedPermissions = properties["permissions"]?.split(",")?.map { it.trim() } ?: emptyList(), ) } @@ -63,19 +68,15 @@ open class ScriptRuntime( return readModuleInfo(inputStream.bufferedReader()) } - fun reload(path: String, content: String) { - unload(path) - load(path, content) - } - - private fun unload(path: String) { - val module = modules[path] ?: return + fun unload(scriptPath: String) { + val module = modules[scriptPath] ?: return + logger.info("Unloading module $scriptPath") module.unload() - modules.remove(path) + modules.remove(scriptPath) } - fun load(path: String, content: String): JSModule? { - logger.info("Loading module $path") + fun load(scriptPath: String, content: String): JSModule? { + logger.info("Loading module $scriptPath") return runCatching { JSModule( scriptRuntime = this, @@ -85,10 +86,10 @@ open class ScriptRuntime( load { buildModuleObject(this, this@apply) } - modules[path] = this + modules[scriptPath] = this } }.onFailure { - logger.error("Failed to load module $path", it) + logger.error("Failed to load module $scriptPath", it) }.getOrNull() } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptingLogger.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptingLogger.kt new file mode 100644 index 000000000..f94060955 --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptingLogger.kt @@ -0,0 +1,40 @@ +package me.rhunk.snapenhance.common.scripting + +import me.rhunk.snapenhance.common.logger.AbstractLogger +import me.rhunk.snapenhance.common.logger.LogChannel + +class ScriptingLogger( + private val logger: AbstractLogger +) { + companion object { + private val TAG = LogChannel.SCRIPTING.channel + } + + fun debug(message: Any?, tag: String = TAG) { + logger.debug(message, tag) + } + + fun error(message: Any?, tag: String = TAG) { + logger.error(message, tag) + } + + fun error(message: Any?, throwable: Throwable, tag: String = TAG) { + logger.error(message, throwable, tag) + } + + fun info(message: Any?, tag: String = TAG) { + logger.info(message, tag) + } + + fun verbose(message: Any?, tag: String = TAG) { + logger.verbose(message, tag) + } + + fun warn(message: Any?, tag: String = TAG) { + logger.warn(message, tag) + } + + fun assert(message: Any?, tag: String = TAG) { + logger.assert(message, tag) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/AbstractBinding.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/AbstractBinding.kt new file mode 100644 index 000000000..0baf42fe1 --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/AbstractBinding.kt @@ -0,0 +1,14 @@ +package me.rhunk.snapenhance.common.scripting.bindings + +abstract class AbstractBinding( + val name: String, + val side: BindingSide +) { + lateinit var context: BindingsContext + + open fun onInit() {} + + open fun onDispose() {} + + abstract fun getObject(): Any +} \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingSide.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingSide.kt new file mode 100644 index 000000000..2ab8c9710 --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingSide.kt @@ -0,0 +1,15 @@ +package me.rhunk.snapenhance.common.scripting.bindings + +enum class BindingSide( + val key: String +) { + COMMON("common"), + CORE("core"), + MANAGER("manager"); + + companion object { + fun fromKey(key: String): BindingSide { + return entries.firstOrNull { it.key == key } ?: COMMON + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingsContext.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingsContext.kt new file mode 100644 index 000000000..8b250bb79 --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/bindings/BindingsContext.kt @@ -0,0 +1,9 @@ +package me.rhunk.snapenhance.common.scripting.bindings + +import me.rhunk.snapenhance.common.scripting.ScriptRuntime +import me.rhunk.snapenhance.common.scripting.type.ModuleInfo + +class BindingsContext( + val moduleInfo: ModuleInfo, + val runtime: ScriptRuntime +) \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/ConfigInterface.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/ConfigInterface.kt index bc4f7a1dc..acc54acd3 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/ConfigInterface.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/ConfigInterface.kt @@ -1,5 +1,7 @@ package me.rhunk.snapenhance.common.scripting.impl +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide import org.mozilla.javascript.annotations.JSFunction @@ -18,7 +20,8 @@ enum class ConfigTransactionType( } -abstract class ConfigInterface { +@Suppress("unused") +abstract class ConfigInterface : AbstractBinding("config", BindingSide.COMMON) { @JSFunction fun get(key: String): String? = get(key, null) @JSFunction abstract fun get(key: String, defaultValue: Any?): String? @@ -70,5 +73,7 @@ abstract class ConfigInterface { @JSFunction abstract fun save() @JSFunction abstract fun load() - @JSFunction abstract fun delete() + @JSFunction abstract fun deleteConfig() + + override fun getObject() = this } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/IPCInterface.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/IPCInterface.kt index 8eb427621..ca2a01156 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/IPCInterface.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/IPCInterface.kt @@ -1,8 +1,11 @@ package me.rhunk.snapenhance.common.scripting.impl -typealias Listener = (Array) -> Unit +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide -abstract class IPCInterface { +typealias Listener = (List) -> Unit + +abstract class IPCInterface : AbstractBinding("ipc", BindingSide.COMMON) { abstract fun on(eventName: String, listener: Listener) abstract fun onBroadcast(channel: String, eventName: String, listener: Listener) @@ -13,5 +16,8 @@ abstract class IPCInterface { @Suppress("unused") fun emit(eventName: String) = emit(eventName, *emptyArray()) @Suppress("unused") - fun emit(channel: String, eventName: String) = broadcast(channel, eventName) + fun broadcast(channel: String, eventName: String) = + broadcast(channel, eventName, *emptyArray()) + + override fun getObject() = this } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt index 1bdeb2ea0..047536a15 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt @@ -7,5 +7,5 @@ data class ModuleInfo( val author: String? = null, val minSnapchatVersion: Long? = null, val minSEVersion: Long? = null, - val grantPermissions: List? = null, + val grantedPermissions: List, ) \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt index 8e222f5aa..a2199184f 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -17,8 +17,8 @@ import me.rhunk.snapenhance.common.data.MessagingGroupInfo import me.rhunk.snapenhance.core.bridge.BridgeClient import me.rhunk.snapenhance.core.bridge.loadFromBridge import me.rhunk.snapenhance.core.data.SnapClassCache -import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent +import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent import me.rhunk.snapenhance.core.util.LSPatchUpdater import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook @@ -142,7 +142,7 @@ class SnapEnhance { bridgeClient.registerMessagingBridge(messagingBridge) features.init() scriptRuntime.connect(bridgeClient.getScriptingInterface()) - scriptRuntime.eachModule { callFunction("module.onBeforeApplicationLoad", androidContext) } + scriptRuntime.eachModule { callFunction("module.onSnapApplicationLoad", androidContext) } syncRemote() } } @@ -151,7 +151,7 @@ class SnapEnhance { measureTimeMillis { with(appContext) { features.onActivityCreate() - scriptRuntime.eachModule { callFunction("module.onSnapActivity", mainActivity!!) } + scriptRuntime.eachModule { callFunction("module.onSnapMainActivityCreate", mainActivity!!) } } }.also { time -> appContext.log.verbose("onActivityCreate took $time") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt index 32362ed6a..bd39c99fb 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt @@ -7,22 +7,21 @@ import me.rhunk.snapenhance.common.scripting.ScriptRuntime import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.scripting.impl.CoreIPC import me.rhunk.snapenhance.core.scripting.impl.CoreScriptConfig -import me.rhunk.snapenhance.core.scripting.impl.ScriptHooker +import me.rhunk.snapenhance.core.scripting.impl.CoreScriptHooker class CoreScriptRuntime( private val modContext: ModContext, logger: AbstractLogger, ): ScriptRuntime(modContext.androidContext, logger) { - private val scriptHookers = mutableListOf() - fun connect(scriptingInterface: IScripting) { + scripting = scriptingInterface scriptingInterface.apply { buildModuleObject = { module -> - module.extras["ipc"] = CoreIPC(this@apply, module.moduleInfo) - module.extras["hooker"] = ScriptHooker(module.moduleInfo, logger, androidContext.classLoader).also { - scriptHookers.add(it) - } - module.extras["config"] = CoreScriptConfig(this@apply, module.moduleInfo) + module.registerBindings( + CoreScriptConfig(), + CoreIPC(), + CoreScriptHooker(), + ) } enabledScripts.forEach { path -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreIPC.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreIPC.kt index 8725e0211..18ae8bb59 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreIPC.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreIPC.kt @@ -1,32 +1,27 @@ package me.rhunk.snapenhance.core.scripting.impl import me.rhunk.snapenhance.bridge.scripting.IPCListener -import me.rhunk.snapenhance.bridge.scripting.IScripting import me.rhunk.snapenhance.common.scripting.impl.IPCInterface import me.rhunk.snapenhance.common.scripting.impl.Listener -import me.rhunk.snapenhance.common.scripting.type.ModuleInfo -class CoreIPC( - private val scripting: IScripting, - private val moduleInfo: ModuleInfo -) : IPCInterface() { +class CoreIPC : IPCInterface() { override fun onBroadcast(channel: String, eventName: String, listener: Listener) { - scripting.registerIPCListener(channel, eventName, object: IPCListener.Stub() { + context.runtime.scripting.registerIPCListener(channel, eventName, object: IPCListener.Stub() { override fun onMessage(args: Array) { - listener(args) + listener(args.toList()) } }) } override fun on(eventName: String, listener: Listener) { - onBroadcast(moduleInfo.name, eventName, listener) + onBroadcast(context.moduleInfo.name, eventName, listener) } override fun emit(eventName: String, vararg args: String?) { - broadcast(moduleInfo.name, eventName, *args) + broadcast(context.moduleInfo.name, eventName, *args) } override fun broadcast(channel: String, eventName: String, vararg args: String?) { - scripting.sendIPCMessage(channel, eventName, args) + context.runtime.scripting.sendIPCMessage(channel, eventName, args) } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptConfig.kt index 81177482d..4baba70e6 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptConfig.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptConfig.kt @@ -1,31 +1,26 @@ package me.rhunk.snapenhance.core.scripting.impl -import me.rhunk.snapenhance.bridge.scripting.IScripting import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface import me.rhunk.snapenhance.common.scripting.impl.ConfigTransactionType -import me.rhunk.snapenhance.common.scripting.type.ModuleInfo -class CoreScriptConfig( - private val scripting: IScripting, - private val moduleInfo: ModuleInfo -): ConfigInterface() { +class CoreScriptConfig: ConfigInterface() { override fun get(key: String, defaultValue: Any?): String? { - return scripting.configTransaction(moduleInfo.name, ConfigTransactionType.GET.key, key, defaultValue.toString(), false) + return context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.GET.key, key, defaultValue.toString(), false) } override fun set(key: String, value: Any?, save: Boolean) { - scripting.configTransaction(moduleInfo.name, ConfigTransactionType.SET.key, key, value.toString(), save) + context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.SET.key, key, value.toString(), save) } override fun save() { - scripting.configTransaction(moduleInfo.name, ConfigTransactionType.SAVE.key, null, null, false) + context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.SAVE.key, null, null, false) } override fun load() { - scripting.configTransaction(moduleInfo.name, ConfigTransactionType.LOAD.key, null, null, false) + context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.LOAD.key, null, null, false) } - override fun delete() { - scripting.configTransaction(moduleInfo.name, ConfigTransactionType.DELETE.key, null, null, false) + override fun deleteConfig() { + context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.DELETE.key, null, null, false) } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/ScriptHooker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptHooker.kt similarity index 88% rename from core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/ScriptHooker.kt rename to core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptHooker.kt index b53c80e32..ca127a950 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/ScriptHooker.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreScriptHooker.kt @@ -1,8 +1,9 @@ package me.rhunk.snapenhance.core.scripting.impl -import me.rhunk.snapenhance.common.logger.AbstractLogger +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide +import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject import me.rhunk.snapenhance.common.scripting.toPrimitiveValue -import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.core.util.hook.HookAdapter import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.Hooker @@ -71,21 +72,20 @@ class ScriptHookCallback( typealias HookCallback = (ScriptHookCallback) -> Unit typealias HookUnhook = () -> Unit -@Suppress("unused", "MemberVisibilityCanBePrivate") -class ScriptHooker( - private val moduleInfo: ModuleInfo, - private val logger: AbstractLogger, - private val classLoader: ClassLoader -) { +@Suppress("unused") +class CoreScriptHooker: AbstractBinding("hooker", BindingSide.CORE) { private val hooks = mutableListOf() - // -- search for class members + val stage = scriptableObject { + putConst("BEFORE", this, "before") + putConst("AFTER", this, "after") + } private fun findClassSafe(className: String): Class<*>? { return runCatching { - classLoader.loadClass(className) + context.runtime.androidContext.classLoader.loadClass(className) }.onFailure { - logger.warn("Failed to load class $className") + context.runtime.logger.warn("Failed to load class $className") }.getOrNull() } @@ -158,4 +158,11 @@ class ScriptHooker( fun hookAllConstructors(className: String, stage: String, callback: HookCallback) = findClassSafe(className)?.let { hookAllConstructors(it, stage, callback) } + + override fun onDispose() { + hooks.forEach { it() } + hooks.clear() + } + + override fun getObject() = this } \ No newline at end of file From 4a4aa28ccf3fd983092ed8409325543a81d180cb Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 24 Dec 2023 17:29:42 +0100 Subject: [PATCH 08/53] refactor: organize imports --- .../snapenhance/ui/manager/sections/features/FeaturesSection.kt | 2 +- .../rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt | 1 - .../core/features/impl/experiments/EndToEndEncryption.kt | 2 +- .../core/features/impl/messaging/PreventMessageSending.kt | 2 +- .../me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt index 13cd1dea0..ff47411e0 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt @@ -12,10 +12,10 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.rounded.Save import androidx.compose.material3.* diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt index 314904822..96a4fd1f3 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.* import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.Composable diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt index 2766f1b28..e56b7ff42 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt @@ -21,8 +21,8 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoWriter import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent -import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent +import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.impl.messaging.Messaging diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/PreventMessageSending.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/PreventMessageSending.kt index 1d18d3d31..0a661df5a 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/PreventMessageSending.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/PreventMessageSending.kt @@ -2,8 +2,8 @@ package me.rhunk.snapenhance.core.features.impl.messaging import me.rhunk.snapenhance.common.data.NotificationType import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor -import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent +import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage 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 index 8583cdb3a..441973e9b 100644 --- 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 @@ -11,9 +11,9 @@ import android.widget.TextView import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader +import me.rhunk.snapenhance.core.features.impl.experiments.ConvertMessageLocally import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger -import me.rhunk.snapenhance.core.features.impl.experiments.ConvertMessageLocally import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.ui.ViewTagState import me.rhunk.snapenhance.core.ui.applyTheme From 3edd6ed72336ca8f4f360405a85d17973881bcfd Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Mon, 25 Dec 2023 02:02:27 +0100 Subject: [PATCH 09/53] feat(scripting): module exports - add currentSide global - add script name rule - add displayName --- .../snapenhance/messaging/ModDatabase.kt | 22 ++++++++++++------- .../scripting/RemoteScriptManager.kt | 2 ++ .../sections/scripting/ScriptsSection.kt | 2 +- .../snapenhance/common/scripting/JSModule.kt | 13 ++++++++++- .../common/scripting/ScriptRuntime.kt | 11 +++++++--- .../common/scripting/type/ModuleInfo.kt | 1 + .../core/scripting/CoreScriptRuntime.kt | 2 ++ 7 files changed, 40 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt index 2a04a68f0..de5ab1e80 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt @@ -64,6 +64,7 @@ class ModDatabase( "scripts" to listOf( "name VARCHAR PRIMARY KEY", "version VARCHAR NOT NULL", + "displayName VARCHAR", "description VARCHAR", "author VARCHAR NOT NULL", "enabled BOOLEAN" @@ -266,6 +267,7 @@ class ModDatabase( ModuleInfo( name = cursor.getStringOrNull("name")!!, version = cursor.getStringOrNull("version")!!, + displayName = cursor.getStringOrNull("displayName"), description = cursor.getStringOrNull("description"), author = cursor.getStringOrNull("author"), grantedPermissions = emptyList() @@ -305,14 +307,18 @@ class ModDatabase( } availableScripts.forEach { script -> - if (!enabledScriptPaths.contains(script.name)) { - database.execSQL("INSERT OR REPLACE INTO scripts (name, version, description, author, enabled) VALUES (?, ?, ?, ?, ?)", arrayOf( - script.name, - script.version, - script.description, - script.author, - 0 - )) + if (!enabledScriptPaths.contains(script.name) || script != enabledScripts.find { it.name == script.name }) { + database.execSQL( + "INSERT OR REPLACE INTO scripts (name, version, displayName, description, author, enabled) VALUES (?, ?, ?, ?, ?, ?)", + arrayOf( + script.name, + script.version, + script.displayName, + script.description, + script.author, + 0 + ) + ) } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt index 83a592d94..fa47d2813 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt @@ -7,6 +7,7 @@ import me.rhunk.snapenhance.bridge.scripting.AutoReloadListener import me.rhunk.snapenhance.bridge.scripting.IPCListener import me.rhunk.snapenhance.bridge.scripting.IScripting import me.rhunk.snapenhance.common.scripting.ScriptRuntime +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface import me.rhunk.snapenhance.common.scripting.impl.ConfigTransactionType import me.rhunk.snapenhance.common.scripting.type.ModuleInfo @@ -63,6 +64,7 @@ class RemoteScriptManager( fun init() { runtime.buildModuleObject = { module -> + putConst("currentSide", this, BindingSide.MANAGER.key) module.registerBindings( ManagerIPC(ipcListeners), InterfaceManager(), diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt index 28f99f0c1..7afda62f5 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt @@ -64,7 +64,7 @@ class ScriptsSection : Section() { .weight(1f) .padding(end = 8.dp) ) { - Text(text = script.name, fontSize = 20.sp,) + Text(text = script.displayName ?: script.name, fontSize = 20.sp,) Text(text = script.description ?: "No description", fontSize = 14.sp,) } IconButton(onClick = { openSettings = !openSettings }) { diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt index e61cb3c38..0cbaf78f2 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt @@ -6,6 +6,7 @@ import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding import me.rhunk.snapenhance.common.scripting.bindings.BindingsContext import me.rhunk.snapenhance.common.scripting.ktx.contextScope import me.rhunk.snapenhance.common.scripting.ktx.putFunction +import me.rhunk.snapenhance.common.scripting.ktx.scriptable import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import org.mozilla.javascript.Function @@ -39,6 +40,7 @@ class JSModule( putConst("info", this, scriptableObject { putConst("name", this, moduleInfo.name) putConst("version", this, moduleInfo.version) + putConst("displayName", this, moduleInfo.displayName) putConst("description", this, moduleInfo.description) putConst("author", this, moduleInfo.author) putConst("minSnapchatVersion", this, moduleInfo.minSnapchatVersion) @@ -145,7 +147,16 @@ class JSModule( moduleObject.putFunction("require") { args -> val bindingName = args?.get(0).toString() - moduleBindings[bindingName]?.getObject() + val (namespace, path) = bindingName.takeIf { + it.startsWith("@") && it.contains("/") + }?.let { + it.substring(1).substringBefore("/") to it.substringAfter("/") + } ?: (null to "") + + when (namespace) { + "modules" -> scriptRuntime.getModuleByName(path)?.moduleObject?.scriptable("module")?.scriptable("exports") + else -> moduleBindings[bindingName]?.getObject() + } } evaluateString(moduleObject, content, moduleInfo.name, 1, null) diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptRuntime.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptRuntime.kt index b9bed87db..b2b52890d 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptRuntime.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ScriptRuntime.kt @@ -54,12 +54,17 @@ open class ScriptRuntime( } return ModuleInfo( - name = properties["name"] ?: throw Exception("Missing module name"), + name = properties["name"]?.also { + if (!it.matches(Regex("[a-z_]+"))) { + throw Exception("Invalid module name : Only lowercase letters and underscores are allowed") + } + } ?: throw Exception("Missing module name"), version = properties["version"] ?: throw Exception("Missing module version"), + displayName = properties["displayName"], description = properties["description"], author = properties["author"], - minSnapchatVersion = properties["minSnapchatVersion"]?.toLong(), - minSEVersion = properties["minSEVersion"]?.toLong(), + minSnapchatVersion = properties["minSnapchatVersion"]?.toLongOrNull(), + minSEVersion = properties["minSEVersion"]?.toLongOrNull(), grantedPermissions = properties["permissions"]?.split(",")?.map { it.trim() } ?: emptyList(), ) } diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt index 047536a15..31cda8d00 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt @@ -3,6 +3,7 @@ package me.rhunk.snapenhance.common.scripting.type data class ModuleInfo( val name: String, val version: String, + val displayName: String? = null, val description: String? = null, val author: String? = null, val minSnapchatVersion: Long? = null, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt index bd39c99fb..cca660802 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt @@ -4,6 +4,7 @@ import me.rhunk.snapenhance.bridge.scripting.AutoReloadListener import me.rhunk.snapenhance.bridge.scripting.IScripting import me.rhunk.snapenhance.common.logger.AbstractLogger import me.rhunk.snapenhance.common.scripting.ScriptRuntime +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.scripting.impl.CoreIPC import me.rhunk.snapenhance.core.scripting.impl.CoreScriptConfig @@ -17,6 +18,7 @@ class CoreScriptRuntime( scripting = scriptingInterface scriptingInterface.apply { buildModuleObject = { module -> + putConst("currentSide", this, BindingSide.CORE.key) module.registerBindings( CoreScriptConfig(), CoreIPC(), From 37becec35047bee5e4493cd03ea26fbac3263492 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Mon, 25 Dec 2023 12:10:29 +0100 Subject: [PATCH 10/53] feat(scripting): permissions - Java Interfaces binding --- .../snapenhance/common/scripting/JSModule.kt | 25 ++++++++--- .../common/scripting/impl/JavaInterfaces.kt | 45 +++++++++++++++++++ .../common/scripting/ktx/RhinoKtx.kt | 9 +++- .../common/scripting/type/ModuleInfo.kt | 18 +++++++- .../common/scripting/type/Permissions.kt | 7 +++ 5 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/JavaInterfaces.kt create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/Permissions.kt diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt index 0cbaf78f2..c7499aaf8 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt @@ -4,11 +4,13 @@ import android.os.Handler import android.widget.Toast import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding import me.rhunk.snapenhance.common.scripting.bindings.BindingsContext +import me.rhunk.snapenhance.common.scripting.impl.JavaInterfaces import me.rhunk.snapenhance.common.scripting.ktx.contextScope import me.rhunk.snapenhance.common.scripting.ktx.putFunction import me.rhunk.snapenhance.common.scripting.ktx.scriptable import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject import me.rhunk.snapenhance.common.scripting.type.ModuleInfo +import me.rhunk.snapenhance.common.scripting.type.Permissions import org.mozilla.javascript.Function import org.mozilla.javascript.NativeJavaObject import org.mozilla.javascript.ScriptableObject @@ -49,6 +51,10 @@ class JSModule( }) }) + registerBindings( + JavaInterfaces(), + ) + moduleObject.putFunction("setField") { args -> val obj = args?.get(0) as? NativeJavaObject ?: return@putFunction Undefined.instance val name = args[1].toString() @@ -74,8 +80,12 @@ class JSModule( moduleObject.putFunction("findClass") { val className = it?.get(0).toString() + val useModClassLoader = it?.getOrNull(1) as? Boolean ?: false + if (useModClassLoader) moduleInfo.ensurePermissionGranted(Permissions.UNSAFE_CLASSLOADER) + runCatching { - classLoader.loadClass(className) + if (useModClassLoader) this::class.java.classLoader?.loadClass(className) + else classLoader.loadClass(className) }.onFailure { throwable -> scriptRuntime.logger.error("Failed to load class $className", throwable) }.getOrNull() @@ -83,7 +93,12 @@ class JSModule( moduleObject.putFunction("type") { args -> val className = args?.get(0).toString() - val clazz = runCatching { classLoader.loadClass(className) }.getOrNull() ?: return@putFunction Undefined.instance + val useModClassLoader = args?.getOrNull(1) as? Boolean ?: false + if (useModClassLoader) moduleInfo.ensurePermissionGranted(Permissions.UNSAFE_CLASSLOADER) + + val clazz = runCatching { + if (useModClassLoader) this::class.java.classLoader?.loadClass(className) else classLoader.loadClass(className) + }.getOrNull() ?: return@putFunction Undefined.instance scriptableObject("JavaClassWrapper") { putFunction("newInstance") newInstance@{ args -> @@ -116,7 +131,7 @@ class JSModule( } moduleObject.putFunction("logError") { args -> - scriptRuntime.logger.error(argsToString(arrayOf(args?.get(0))), args?.get(1) as? Throwable ?: Throwable()) + scriptRuntime.logger.error(argsToString(arrayOf(args?.get(0))), args?.getOrNull(1) as? Throwable ?: Throwable()) Undefined.instance } @@ -179,8 +194,8 @@ class JSModule( contextScope { name.split(".").also { split -> val function = split.dropLast(1).fold(moduleObject) { obj, key -> - obj.get(key, obj) as? ScriptableObject ?: return@contextScope - }.get(split.last(), moduleObject) as? Function ?: return@contextScope + obj.get(key, obj) as? ScriptableObject ?: return@contextScope Unit + }.get(split.last(), moduleObject) as? Function ?: return@contextScope Unit runCatching { function.call(this, moduleObject, moduleObject, args) diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/JavaInterfaces.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/JavaInterfaces.kt new file mode 100644 index 000000000..4cfd75ad0 --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/JavaInterfaces.kt @@ -0,0 +1,45 @@ +package me.rhunk.snapenhance.common.scripting.impl + +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide +import me.rhunk.snapenhance.common.scripting.ktx.contextScope +import me.rhunk.snapenhance.common.scripting.ktx.putFunction +import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject +import java.lang.reflect.Proxy + +class JavaInterfaces : AbstractBinding("java-interfaces", BindingSide.COMMON) { + override fun getObject() = scriptableObject { + putFunction("runnable") { + val function = it?.get(0) as? org.mozilla.javascript.Function ?: return@putFunction null + Runnable { + contextScope { + function.call( + this, + this@scriptableObject, + this@scriptableObject, + emptyArray() + ) + } + } + } + + putFunction("newProxy") { arguments -> + val javaInterface = arguments?.get(0) as? Class<*> ?: return@putFunction null + val function = arguments[1] as? org.mozilla.javascript.Function ?: return@putFunction null + + Proxy.newProxyInstance( + javaInterface.classLoader, + arrayOf(javaInterface) + ) { instance, method, args -> + contextScope { + function.call( + this, + this@scriptableObject, + this@scriptableObject, + arrayOf(instance, method.name, (args ?: emptyArray()).toList()) + ) + } + } + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ktx/RhinoKtx.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ktx/RhinoKtx.kt index 39b469e28..a219921c6 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ktx/RhinoKtx.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ktx/RhinoKtx.kt @@ -4,12 +4,17 @@ import org.mozilla.javascript.Context import org.mozilla.javascript.Function import org.mozilla.javascript.Scriptable import org.mozilla.javascript.ScriptableObject +import org.mozilla.javascript.Wrapper -fun contextScope(f: Context.() -> Unit) { +fun contextScope(f: Context.() -> Any?): Any? { val context = Context.enter() context.optimizationLevel = -1 try { - context.f() + return context.f().let { + if (it is Wrapper) { + it.unwrap() + } else it + } } finally { Context.exit() } diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt index 31cda8d00..2219f0290 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/ModuleInfo.kt @@ -9,4 +9,20 @@ data class ModuleInfo( val minSnapchatVersion: Long? = null, val minSEVersion: Long? = null, val grantedPermissions: List, -) \ No newline at end of file +) { + override fun equals(other: Any?): Boolean { + if (other !is ModuleInfo) return false + if (other === this) return true + return name == other.name && + version == other.version && + displayName == other.displayName && + description == other.description && + author == other.author + } + + fun ensurePermissionGranted(permission: Permissions) { + if (!grantedPermissions.contains(permission.key)) { + throw AssertionError("Permission $permission is not granted") + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/Permissions.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/Permissions.kt new file mode 100644 index 000000000..811b832b4 --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/type/Permissions.kt @@ -0,0 +1,7 @@ +package me.rhunk.snapenhance.common.scripting.type + +enum class Permissions( + val key: String, +) { + UNSAFE_CLASSLOADER("unsafe-classloader"), +} \ No newline at end of file From 72c9b92a3e07d6b60832204aac661c989fe841df Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Mon, 25 Dec 2023 23:10:31 +0100 Subject: [PATCH 11/53] feat(scripting): integrated ui --- .../scripting/RemoteScriptManager.kt | 2 - .../impl/ui/components/impl/ActionNode.kt | 15 ----- .../sections/scripting/ScriptsSection.kt | 16 ++---- common/src/main/assets/lang/en_US.json | 4 ++ .../common/config/impl/Scripting.kt | 1 + .../snapenhance/common/scripting/JSModule.kt | 2 + .../scripting/ui/EnumScriptInterface.kt | 11 ++++ .../common/scripting}/ui/InterfaceManager.kt | 37 ++++++++---- .../common/scripting/ui}/ScriptInterface.kt | 11 ++-- .../common/scripting}/ui/components/Node.kt | 2 +- .../scripting}/ui/components/NodeType.kt | 3 +- .../ui/components/impl/ActionNode.kt | 15 +++++ .../ui/components/impl/RowColumnNode.kt | 6 +- .../core/manager/impl/FeatureManager.kt | 2 +- .../ui/menu/{impl => }/MenuViewInjector.kt | 56 +++++++++++-------- .../core/ui/menu/impl/FriendFeedInfoMenu.kt | 34 +++++++++++ 16 files changed, 141 insertions(+), 76 deletions(-) delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/impl/ActionNode.kt create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/EnumScriptInterface.kt rename {app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl => common/src/main/kotlin/me/rhunk/snapenhance/common/scripting}/ui/InterfaceManager.kt (67%) rename {app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting => common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui}/ScriptInterface.kt (93%) rename {app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl => common/src/main/kotlin/me/rhunk/snapenhance/common/scripting}/ui/components/Node.kt (95%) rename {app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl => common/src/main/kotlin/me/rhunk/snapenhance/common/scripting}/ui/components/NodeType.kt (64%) create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/impl/ActionNode.kt rename {app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl => common/src/main/kotlin/me/rhunk/snapenhance/common/scripting}/ui/components/impl/RowColumnNode.kt (88%) rename core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/{impl => }/MenuViewInjector.kt (81%) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt index fa47d2813..d7319b262 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt @@ -14,7 +14,6 @@ import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.scripting.impl.IPCListeners import me.rhunk.snapenhance.scripting.impl.ManagerIPC import me.rhunk.snapenhance.scripting.impl.ManagerScriptConfig -import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager import java.io.File import java.io.InputStream import kotlin.system.exitProcess @@ -67,7 +66,6 @@ class RemoteScriptManager( putConst("currentSide", this, BindingSide.MANAGER.key) module.registerBindings( ManagerIPC(ipcListeners), - InterfaceManager(), ManagerScriptConfig(this@RemoteScriptManager) ) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/impl/ActionNode.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/impl/ActionNode.kt deleted file mode 100644 index dd2cc9ba0..000000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/impl/ActionNode.kt +++ /dev/null @@ -1,15 +0,0 @@ -package me.rhunk.snapenhance.scripting.impl.ui.components.impl - -import me.rhunk.snapenhance.scripting.impl.ui.components.Node -import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType - -enum class ActionType { - LAUNCHED, - DISPOSE -} - -class ActionNode( - val actionType: ActionType, - val key: Any = Unit, - val callback: () -> Unit -): Node(NodeType.ACTION) \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt index 7afda62f5..90f423fb6 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt @@ -20,7 +20,9 @@ import androidx.documentfile.provider.DocumentFile import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.rhunk.snapenhance.common.scripting.type.ModuleInfo -import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager +import me.rhunk.snapenhance.common.scripting.ui.EnumScriptInterface +import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager +import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper import me.rhunk.snapenhance.ui.util.chooseFolder @@ -140,22 +142,14 @@ class ScriptsSection : Section() { @Composable fun ScriptSettings(script: ModuleInfo) { - var settingsError by remember { - mutableStateOf(null as Throwable?) - } - val settingsInterface = remember { val module = context.scriptManager.runtime.getModuleByName(script.name) ?: return@remember null - runCatching { - (module.getBinding(InterfaceManager::class))?.buildInterface("settings") - }.onFailure { - settingsError = it - }.getOrNull() + (module.getBinding(InterfaceManager::class))?.buildInterface(EnumScriptInterface.SETTINGS) } if (settingsInterface == null) { Text( - text = settingsError?.message ?: "This module does not have any settings", + text = "This module does not have any settings", style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(8.dp) ) diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index b3bbbceaf..ef17f75c0 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -703,6 +703,10 @@ "name": "Auto Reload", "description": "Automatically reloads scripts when they change" }, + "integrated_ui": { + "name": "Integrated UI", + "description": "Allows scripts to add custom UI components to Snapchat" + }, "disable_log_anonymization": { "name": "Disable Log Anonymization", "description": "Disables the anonymization of logs" diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Scripting.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Scripting.kt index 83b3ec169..08453cd63 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Scripting.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Scripting.kt @@ -7,5 +7,6 @@ class Scripting : ConfigContainer() { val developerMode = boolean("developer_mode", false) { requireRestart() } val moduleFolder = string("module_folder", "modules") { addFlags(ConfigFlag.FOLDER); requireRestart() } val autoReload = unique("auto_reload", "snapchat_only", "all") + val integratedUI = boolean("integrated_ui", false) { requireRestart() } val disableLogAnonymization = boolean("disable_log_anonymization", false) { requireRestart() } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt index c7499aaf8..9262fe933 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt @@ -11,6 +11,7 @@ import me.rhunk.snapenhance.common.scripting.ktx.scriptable import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.common.scripting.type.Permissions +import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager import org.mozilla.javascript.Function import org.mozilla.javascript.NativeJavaObject import org.mozilla.javascript.ScriptableObject @@ -53,6 +54,7 @@ class JSModule( registerBindings( JavaInterfaces(), + InterfaceManager(), ) moduleObject.putFunction("setField") { args -> diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/EnumScriptInterface.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/EnumScriptInterface.kt new file mode 100644 index 000000000..b3114ead8 --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/EnumScriptInterface.kt @@ -0,0 +1,11 @@ +package me.rhunk.snapenhance.common.scripting.ui + +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide + +enum class EnumScriptInterface( + val key: String, + val side: BindingSide +) { + SETTINGS("settings", BindingSide.MANAGER), + FRIEND_FEED_CONTEXT_MENU("friendFeedContextMenu", BindingSide.CORE), +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/InterfaceManager.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/InterfaceManager.kt similarity index 67% rename from app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/InterfaceManager.kt rename to common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/InterfaceManager.kt index fbfc9c60a..da90a91b7 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/InterfaceManager.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/InterfaceManager.kt @@ -1,13 +1,14 @@ -package me.rhunk.snapenhance.scripting.impl.ui +package me.rhunk.snapenhance.common.scripting.ui import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding import me.rhunk.snapenhance.common.scripting.bindings.BindingSide import me.rhunk.snapenhance.common.scripting.ktx.contextScope -import me.rhunk.snapenhance.scripting.impl.ui.components.Node -import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType -import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionNode -import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionType -import me.rhunk.snapenhance.scripting.impl.ui.components.impl.RowColumnNode +import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject +import me.rhunk.snapenhance.common.scripting.ui.components.Node +import me.rhunk.snapenhance.common.scripting.ui.components.NodeType +import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionNode +import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionType +import me.rhunk.snapenhance.common.scripting.ui.components.impl.RowColumnNode import org.mozilla.javascript.Function import org.mozilla.javascript.annotations.JSFunction @@ -73,24 +74,36 @@ class InterfaceBuilder { -class InterfaceManager : AbstractBinding("interface-manager", BindingSide.MANAGER) { - private val interfaces = mutableMapOf InterfaceBuilder?>() +class InterfaceManager : AbstractBinding("interface-manager", BindingSide.COMMON) { + private val interfaces = mutableMapOf) -> InterfaceBuilder?>() - fun buildInterface(name: String): InterfaceBuilder? { - return interfaces[name]?.invoke() + fun buildInterface(scriptInterface: EnumScriptInterface, args: Map = emptyMap()): InterfaceBuilder? { + return runCatching { + interfaces[scriptInterface.key]?.invoke(args) + }.onFailure { + context.runtime.logger.error("Failed to build interface ${scriptInterface.key} for ${context.moduleInfo.name}", it) + }.getOrNull() } override fun onDispose() { interfaces.clear() } + fun hasInterface(scriptInterfaces: EnumScriptInterface): Boolean { + return interfaces.containsKey(scriptInterfaces.key) + } + @Suppress("unused") @JSFunction fun create(name: String, callback: Function) { - interfaces[name] = { + interfaces[name] = { args -> val interfaceBuilder = InterfaceBuilder() runCatching { contextScope { - callback.call(this, callback, callback, arrayOf(interfaceBuilder)) + callback.call(this, callback, callback, arrayOf(interfaceBuilder, scriptableObject { + args.forEach { (key, value) -> + putConst(key,this, value) + } + })) } interfaceBuilder }.onFailure { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptInterface.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/ScriptInterface.kt similarity index 93% rename from app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptInterface.kt rename to common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/ScriptInterface.kt index a11d5a335..ff11dddf3 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptInterface.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/ScriptInterface.kt @@ -1,4 +1,4 @@ -package me.rhunk.snapenhance.ui.manager.sections.scripting +package me.rhunk.snapenhance.common.scripting.ui import androidx.compose.foundation.layout.* import androidx.compose.material3.OutlinedButton @@ -13,11 +13,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch import me.rhunk.snapenhance.common.logger.AbstractLogger -import me.rhunk.snapenhance.scripting.impl.ui.InterfaceBuilder -import me.rhunk.snapenhance.scripting.impl.ui.components.Node -import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType -import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionNode -import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionType +import me.rhunk.snapenhance.common.scripting.ui.components.Node +import me.rhunk.snapenhance.common.scripting.ui.components.NodeType +import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionNode +import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionType import kotlin.math.abs diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/Node.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/Node.kt similarity index 95% rename from app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/Node.kt rename to common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/Node.kt index e127c26b9..9e534c0eb 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/Node.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/Node.kt @@ -1,4 +1,4 @@ -package me.rhunk.snapenhance.scripting.impl.ui.components +package me.rhunk.snapenhance.common.scripting.ui.components open class Node( val type: NodeType, diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/NodeType.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/NodeType.kt similarity index 64% rename from app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/NodeType.kt rename to common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/NodeType.kt index d3dde3723..f2086d0f4 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/NodeType.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/NodeType.kt @@ -1,4 +1,5 @@ -package me.rhunk.snapenhance.scripting.impl.ui.components +package me.rhunk.snapenhance.common.scripting.ui.components + enum class NodeType { ROW, COLUMN, diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/impl/ActionNode.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/impl/ActionNode.kt new file mode 100644 index 000000000..6a02ea8ba --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/impl/ActionNode.kt @@ -0,0 +1,15 @@ +package me.rhunk.snapenhance.common.scripting.ui.components.impl + +import me.rhunk.snapenhance.common.scripting.ui.components.Node +import me.rhunk.snapenhance.common.scripting.ui.components.NodeType + +enum class ActionType { + LAUNCHED, + DISPOSE +} + +class ActionNode( + val actionType: ActionType, + val key: Any = Unit, + val callback: () -> Unit +): Node(NodeType.ACTION) \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/impl/RowColumnNode.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/impl/RowColumnNode.kt similarity index 88% rename from app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/impl/RowColumnNode.kt rename to common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/impl/RowColumnNode.kt index ce6bb8612..131d8ecce 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/impl/RowColumnNode.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/impl/RowColumnNode.kt @@ -1,9 +1,9 @@ -package me.rhunk.snapenhance.scripting.impl.ui.components.impl +package me.rhunk.snapenhance.common.scripting.ui.components.impl import androidx.compose.foundation.layout.Arrangement import androidx.compose.ui.Alignment -import me.rhunk.snapenhance.scripting.impl.ui.components.Node -import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType +import me.rhunk.snapenhance.common.scripting.ui.components.Node +import me.rhunk.snapenhance.common.scripting.ui.components.NodeType class RowColumnNode( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt index d63c8caf3..058f12562 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt @@ -25,7 +25,7 @@ import me.rhunk.snapenhance.core.features.impl.tweaks.UnsaveableMessages import me.rhunk.snapenhance.core.features.impl.ui.* import me.rhunk.snapenhance.core.logger.CoreLogger import me.rhunk.snapenhance.core.manager.Manager -import me.rhunk.snapenhance.core.ui.menu.impl.MenuViewInjector +import me.rhunk.snapenhance.core.ui.menu.MenuViewInjector import kotlin.reflect.KClass import kotlin.system.measureTimeMillis diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/MenuViewInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt similarity index 81% rename from core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/MenuViewInjector.kt rename to core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt index 97d5be2a8..6aad58cd7 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/MenuViewInjector.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt @@ -1,4 +1,4 @@ -package me.rhunk.snapenhance.core.ui.menu.impl +package me.rhunk.snapenhance.core.ui.menu import android.annotation.SuppressLint import android.view.Gravity @@ -6,12 +6,13 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout +import android.widget.ScrollView import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.ui.ViewTagState -import me.rhunk.snapenhance.core.ui.menu.AbstractMenu +import me.rhunk.snapenhance.core.ui.menu.impl.* import me.rhunk.snapenhance.core.util.ktx.getIdentifier import java.lang.reflect.Modifier import kotlin.reflect.KClass @@ -27,14 +28,18 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar @SuppressLint("ResourceType") override fun asyncOnActivityCreate() { - menuMap[OperaContextActionMenu::class] = OperaContextActionMenu() - menuMap[OperaDownloadIconMenu::class] = OperaDownloadIconMenu() - menuMap[SettingsGearInjector::class] = SettingsGearInjector() - menuMap[FriendFeedInfoMenu::class] = FriendFeedInfoMenu() - menuMap[ChatActionMenu::class] = ChatActionMenu() - menuMap[SettingsMenu::class] = SettingsMenu() - - menuMap.values.forEach { it.context = context; it.init() } + arrayOf( + OperaContextActionMenu(), + OperaDownloadIconMenu(), + SettingsGearInjector(), + FriendFeedInfoMenu(), + ChatActionMenu(), + SettingsMenu() + ).forEach { + menuMap[it::class] = it.also { + it.context = context; it.init() + } + } val messaging = context.feature(Messaging::class) @@ -90,26 +95,29 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar }) } - val viewList = mutableListOf() context.runOnUiThread { - menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, injectedLayout) { view -> - view.layoutParams = LinearLayout.LayoutParams( + injectedLayout.addView(ScrollView(injectedLayout.context).apply { + layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { - setMargins(0, 5, 0, 5) + weight = 1f; + setMargins(0, 100, 0, 0) } - viewList.add(view) - } - - viewList.add(View(injectedLayout.context).apply { - layoutParams = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - 30 - ) - }) - viewList.reversed().forEach { injectedLayout.addView(it, 0) } + addView(LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, injectedLayout) { view -> + view.layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(0, 5, 0, 5) + } + addView(view) + } + }) + }, 0) } event.view = injectedLayout diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt index 2bfddd4d1..25b1d4048 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt @@ -9,8 +9,10 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.CompoundButton +import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.Switch +import androidx.compose.runtime.remember import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -21,6 +23,10 @@ import me.rhunk.snapenhance.common.data.MessageUpdate import me.rhunk.snapenhance.common.database.impl.ConversationMessage import me.rhunk.snapenhance.common.database.impl.FriendInfo import me.rhunk.snapenhance.common.database.impl.UserConversationLink +import me.rhunk.snapenhance.common.scripting.ui.EnumScriptInterface +import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager +import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface +import me.rhunk.snapenhance.common.ui.createComposeView import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie import me.rhunk.snapenhance.core.features.impl.messaging.Messaging @@ -326,5 +332,33 @@ class FriendFeedInfoMenu : AbstractMenu() { } }) } + + if (context.config.scripting.integratedUI.get()) { + context.scriptRuntime.eachModule { + val interfaceManager = getBinding(InterfaceManager::class) + ?.takeIf { + it.hasInterface(EnumScriptInterface.FRIEND_FEED_CONTEXT_MENU) + } ?: return@eachModule + + viewConsumer(LinearLayout(view.context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + + applyTheme(view.width, hasRadius = true) + + orientation = LinearLayout.VERTICAL + addView(createComposeView(view.context) { + ScriptInterface(interfaceBuilder = remember { + interfaceManager.buildInterface(EnumScriptInterface.FRIEND_FEED_CONTEXT_MENU, mapOf( + "conversationId" to conversationId, + "userId" to targetUser + )) + } ?: return@createComposeView) + }) + }) + } + } } } \ No newline at end of file From fe5c6306e1c8fd568823f82e6f52aa8359bb81f7 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Tue, 26 Dec 2023 13:09:52 +0100 Subject: [PATCH 12/53] feat(scripting): compose alert dialog --- .../common/scripting/ui/InterfaceManager.kt | 22 ++++++++++++++- .../common/ui/ComposeViewFactory.kt | 26 ++++++++++++++++++ .../core/action/impl/ExportChatMessages.kt | 27 +++++-------------- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/InterfaceManager.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/InterfaceManager.kt index da90a91b7..e2c7ae7f5 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/InterfaceManager.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/InterfaceManager.kt @@ -1,5 +1,8 @@ package me.rhunk.snapenhance.common.scripting.ui +import android.app.Activity +import android.app.AlertDialog +import androidx.compose.runtime.remember import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding import me.rhunk.snapenhance.common.scripting.bindings.BindingSide import me.rhunk.snapenhance.common.scripting.ktx.contextScope @@ -9,6 +12,7 @@ import me.rhunk.snapenhance.common.scripting.ui.components.NodeType import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionNode import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionType import me.rhunk.snapenhance.common.scripting.ui.components.impl.RowColumnNode +import me.rhunk.snapenhance.common.ui.createComposeAlertDialog import org.mozilla.javascript.Function import org.mozilla.javascript.annotations.JSFunction @@ -74,6 +78,7 @@ class InterfaceBuilder { +@Suppress("unused") class InterfaceManager : AbstractBinding("interface-manager", BindingSide.COMMON) { private val interfaces = mutableMapOf) -> InterfaceBuilder?>() @@ -93,7 +98,6 @@ class InterfaceManager : AbstractBinding("interface-manager", BindingSide.COMMON return interfaces.containsKey(scriptInterfaces.key) } - @Suppress("unused") @JSFunction fun create(name: String, callback: Function) { interfaces[name] = { args -> val interfaceBuilder = InterfaceBuilder() @@ -112,5 +116,21 @@ class InterfaceManager : AbstractBinding("interface-manager", BindingSide.COMMON } } + @JSFunction fun createAlertDialog(activity: Activity, builder: (AlertDialog.Builder) -> Unit, callback: (interfaceBuilder: InterfaceBuilder, alertDialog: AlertDialog) -> Unit): AlertDialog { + return createComposeAlertDialog(activity, builder = builder) { alertDialog -> + ScriptInterface(interfaceBuilder = remember { + InterfaceBuilder().also { + contextScope { + callback(it, alertDialog) + } + } + }) + } + } + + @JSFunction fun createAlertDialog(activity: Activity, callback: (interfaceBuilder: InterfaceBuilder, alertDialog: AlertDialog) -> Unit): AlertDialog { + return createAlertDialog(activity, {}, callback) + } + override fun getObject() = this } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/ComposeViewFactory.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/ComposeViewFactory.kt index 28c01e3bc..c4779d26f 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/ComposeViewFactory.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/ComposeViewFactory.kt @@ -1,12 +1,18 @@ package me.rhunk.snapenhance.common.ui +import android.app.AlertDialog import android.content.Context import android.os.Bundle +import android.view.WindowManager import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.Recomposer +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.AndroidUiDispatcher import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy @@ -58,6 +64,26 @@ fun createComposeView(context: Context, content: @Composable () -> Unit) = Compo } } +fun createComposeAlertDialog(context: Context, builder: AlertDialog.Builder.() -> Unit = {}, content: @Composable (alertDialog: AlertDialog) -> Unit): AlertDialog { + lateinit var alertDialog: AlertDialog + + return AlertDialog.Builder(context) + .apply(builder) + .setView(createComposeView(context) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surface + ) { + content(alertDialog) + } + }) + .create().apply { + alertDialog = this + window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) + } +} + private class OverlayLifecycleOwner : SavedStateRegistryOwner { private var mLifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) private var mSavedStateRegistryController: SavedStateRegistryController = diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportChatMessages.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportChatMessages.kt index 077cff2b1..75f43ffe0 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportChatMessages.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportChatMessages.kt @@ -3,7 +3,6 @@ package me.rhunk.snapenhance.core.action.impl import android.app.AlertDialog import android.content.DialogInterface import android.os.Environment -import android.view.WindowManager import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -26,7 +25,7 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.* import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.database.impl.FriendFeedEntry -import me.rhunk.snapenhance.common.ui.createComposeView +import me.rhunk.snapenhance.common.ui.createComposeAlertDialog import me.rhunk.snapenhance.core.action.AbstractAction import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.logger.CoreLogger @@ -255,24 +254,12 @@ class ExportChatMessages : AbstractAction() { override fun run() { context.coroutineScope.launch(Dispatchers.Main) { - lateinit var exporterDialog: AlertDialog - ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) - .setTitle(translation["select_conversation"]) - .setView(createComposeView(context.mainActivity!!) { - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surface - ) { - ExporterDialog { exporterDialog } - } - }) - .create().apply { - exporterDialog = this - setCanceledOnTouchOutside(false) - show() - window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) - window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) - } + createComposeAlertDialog(context.mainActivity!!) { alertDialog -> + ExporterDialog { alertDialog } + }.apply { + setCanceledOnTouchOutside(false) + show() + } } } From f55e3839ea530353cc0c45089b7a603043b69bd0 Mon Sep 17 00:00:00 2001 From: auth <64337177+authorisation@users.noreply.github.com> Date: Tue, 26 Dec 2023 19:00:32 +0100 Subject: [PATCH 13/53] readme: switch translation host --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 81f6b2d78..22d8d9605 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
- [![Build](https://img.shields.io/github/actions/workflow/status/rhunk/SnapEnhance/beta.yml?branch=dev&logo=github&label=Build)](https://github.com/rhunk/SnapEnhance/actions/workflows/android.yml?query=branch%3Amain+event%3Apush+is%3Acompleted) [![Total](https://shields.io/github/downloads/rhunk/SnapEnhance/total?logo=Bookmeter&label=Downloads&logoColor=Green&color=Green)](https://github.com/rhunk/snapenhance/releases) [![Crowdin](https://badges.crowdin.net/snapenhance/localized.svg)](https://crowdin.com/project/snapenhance) + [![Build](https://img.shields.io/github/actions/workflow/status/rhunk/SnapEnhance/beta.yml?branch=dev&logo=github&label=Build)](https://github.com/rhunk/SnapEnhance/actions/workflows/android.yml?query=branch%3Amain+event%3Apush+is%3Acompleted) [![Total](https://shields.io/github/downloads/rhunk/SnapEnhance/total?logo=Bookmeter&label=Downloads&logoColor=Green&color=Green)](https://github.com/rhunk/snapenhance/releases) [![Translation status](https://hosted.weblate.org/widget/snapenhance/app/svg-badge.svg)](https://hosted.weblate.org/engage/snapenhance/) # SnapEnhance SnapEnhance is an Xposed mod that enhances your Snapchat experience.

@@ -137,6 +137,12 @@ We no longer offer official LSPatch binaries for obvious reasons. However, you'r - No, this will cause some severe issues, and the mod will not be able to inject. +
+ How can I translate SnapEnhance into my language? + + - We have a [Weblate](https://hosted.weblate.org/projects/snapenhance/app/) hosted repo, feel free to submit your translations there. +
+ ## Privacy We do not collect any user information. However, please be aware that third-party libraries may collect data as described in their respective privacy policies.
@@ -171,7 +177,6 @@ Thanks to everyone involved including the [third-party libraries](https://github - [TheVisual](https://github.com/TheVisual) - [CanerKaraca23](https://github.com/CanerKaraca23) - ## Donate - LTC: LbBnT9GxgnFhwy891EdDKqGmpn7XtduBdE - BCH: qpu57a05kqljjadvpgjc6t894apprvth9slvlj4vpj From a249d41887937b40b2d3fb80372ab31c5c60bd31 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Wed, 27 Dec 2023 11:55:29 +0100 Subject: [PATCH 14/53] feat(scripting): sleep --- .../me/rhunk/snapenhance/ui/manager/Navigation.kt | 4 ++-- .../ui/manager/sections/scripting/ScriptsSection.kt | 13 +++++++++++-- .../rhunk/snapenhance/common/scripting/JSModule.kt | 6 ++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt index d547565f3..f37d9500a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt @@ -39,8 +39,8 @@ class Navigation( navHostController, startDestination = startDestination.route, Modifier.padding(innerPadding), - enterTransition = { fadeIn(tween(200)) }, - exitTransition = { fadeOut(tween(200)) } + enterTransition = { fadeIn(tween(100)) }, + exitTransition = { fadeOut(tween(100)) } ) { sections.forEach { (_, instance) -> instance.navController = navHostController diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt index 90f423fb6..1f7423c2a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt @@ -17,8 +17,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.common.scripting.ui.EnumScriptInterface import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager @@ -49,7 +51,7 @@ class ScriptsSection : Section() { Card( modifier = Modifier .fillMaxWidth() - .padding(12.dp), + .padding(8.dp), elevation = CardDefaults.cardElevation() ) { Row( @@ -179,7 +181,11 @@ class ScriptsSection : Section() { } LaunchedEffect(Unit) { - syncScripts() + refreshing = true + withContext(Dispatchers.IO) { + syncScripts() + refreshing = false + } } val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = { @@ -230,6 +236,9 @@ class ScriptsSection : Section() { items(scriptModules.size) { index -> ModuleItem(scriptModules[index]) } + item { + Spacer(modifier = Modifier.height(200.dp)) + } } PullRefreshIndicator( diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt index 9262fe933..0417bdea8 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt @@ -80,6 +80,12 @@ class JSModule( field.get(obj.unwrap()) } + moduleObject.putFunction("sleep") { args -> + val time = args?.get(0) as? Number ?: return@putFunction Undefined.instance + Thread.sleep(time.toLong()) + Undefined.instance + } + moduleObject.putFunction("findClass") { val className = it?.get(0).toString() val useModClassLoader = it?.getOrNull(1) as? Boolean ?: false From 03736d574f155f50b6c06e30f69bdb84827d5a6b Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Wed, 27 Dec 2023 12:01:29 +0100 Subject: [PATCH 15/53] perf(app): log viewer --- .../kotlin/me/rhunk/snapenhance/LogManager.kt | 23 ++-- .../manager/sections/home/HomeSubSection.kt | 113 ++++++++++++------ 2 files changed, 90 insertions(+), 46 deletions(-) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt index e9e7a84aa..a87170b0a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt @@ -70,21 +70,26 @@ class LogReader( fun incrementLineCount() { randomAccessFile.seek(randomAccessFile.length()) - startLineIndexes.add(randomAccessFile.filePointer) + startLineIndexes.add(randomAccessFile.filePointer + 1) lineCount++ } private fun queryLineCount(): Int { randomAccessFile.seek(0) - var lines = 0 - var lastIndex: Long - while (true) { - lastIndex = randomAccessFile.filePointer - readLogLine() ?: break - startLineIndexes.add(lastIndex) - lines++ + var lineCount = 0 + var lastPointer: Long + var line: String? + + while (randomAccessFile.also { + lastPointer = it.filePointer + }.readLine().also { line = it } != null) { + if (line?.startsWith('|') == true) { + lineCount++ + startLineIndexes.add(lastPointer + 1) + } } - return lines + + return lineCount } private fun getLine(index: Int): String? { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt index d376f1dc0..f797549b4 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt @@ -14,7 +14,6 @@ import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Report import androidx.compose.material.icons.outlined.Warning -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -30,16 +29,20 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import me.rhunk.snapenhance.LogReader import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.common.logger.LogChannel import me.rhunk.snapenhance.common.logger.LogLevel +import me.rhunk.snapenhance.ui.util.pullrefresh.PullRefreshIndicator +import me.rhunk.snapenhance.ui.util.pullrefresh.rememberPullRefreshState class HomeSubSection( private val context: RemoteSideContext ) { - private lateinit var logListState: LazyListState + private val logListState by lazy { LazyListState(0) } @Composable fun LogsSection() { @@ -47,27 +50,66 @@ class HomeSubSection( val clipboardManager = LocalClipboardManager.current var lineCount by remember { mutableIntStateOf(0) } var logReader by remember { mutableStateOf(null) } - logListState = remember { LazyListState(0) } + var isRefreshing by remember { mutableStateOf(false) } - Column( + fun refreshLogs() { + coroutineScope.launch(Dispatchers.IO) { + runCatching { + logReader = context.log.newReader { + lineCount++ + } + lineCount = logReader!!.lineCount + }.onFailure { + context.longToast("Failed to read logs!") + } + delay(300) + isRefreshing = false + withContext(Dispatchers.Main) { + logListState.scrollToItem((logListState.layoutInfo.totalItemsCount - 1).takeIf { it >= 0 } ?: return@withContext) + } + } + } + + val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh = { + refreshLogs() + }) + + LaunchedEffect(Unit) { + isRefreshing = true + refreshLogs() + } + + Box( modifier = Modifier .fillMaxSize() ) { LazyColumn( - modifier = Modifier.background(MaterialTheme.colorScheme.surface ) + modifier = Modifier + .background(MaterialTheme.colorScheme.surface) .horizontalScroll(ScrollState(0)), state = logListState ) { + item { + if (lineCount == 0 && logReader != null) { + Text( + text = "No logs found!", + modifier = Modifier.padding(16.dp), + fontSize = 12.sp, + fontWeight = FontWeight.Light + ) + } + } items(lineCount) { index -> - val line = logReader?.getLogLine(index) ?: return@items + val logLine = remember(index) { logReader?.getLogLine(index) } ?: return@items var expand by remember { mutableStateOf(false) } + Box(modifier = Modifier .fillMaxWidth() .pointerInput(Unit) { detectTapGestures( onLongPress = { coroutineScope.launch { - clipboardManager.setText(AnnotatedString(line.message)) + clipboardManager.setText(AnnotatedString(logLine.message)) } }, onTap = { @@ -85,7 +127,7 @@ class HomeSubSection( ) { if (!expand) { Icon( - imageVector = when (line.logLevel) { + imageVector = when (logLine.logLevel) { LogLevel.DEBUG -> Icons.Outlined.BugReport LogLevel.ERROR, LogLevel.ASSERT -> Icons.Outlined.Report LogLevel.INFO, LogLevel.VERBOSE -> Icons.Outlined.Info @@ -95,21 +137,21 @@ class HomeSubSection( ) Text( - text = LogChannel.fromChannel(line.tag)?.shortName ?: line.tag, + text = LogChannel.fromChannel(logLine.tag)?.shortName ?: logLine.tag, modifier = Modifier.padding(start = 4.dp), fontWeight = FontWeight.Light, fontSize = 10.sp, ) Text( - text = line.dateTime, + text = logLine.dateTime, modifier = Modifier.padding(start = 4.dp, end = 4.dp), fontSize = 10.sp ) } Text( - text = line.message.trimIndent(), + text = logLine.message.trimIndent(), fontSize = 10.sp, maxLines = if (expand) Int.MAX_VALUE else 6, overflow = if (expand) TextOverflow.Visible else TextOverflow.Ellipsis, @@ -120,22 +162,11 @@ class HomeSubSection( } } - if (logReader == null) { - CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally)) - } - - LaunchedEffect(Unit) { - coroutineScope.launch(Dispatchers.IO) { - runCatching { - logReader = context.log.newReader { - lineCount++ - } - lineCount = logReader!!.lineCount - }.onFailure { - context.longToast("Failed to read logs!") - } - } - } + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) } } @@ -145,19 +176,27 @@ class HomeSubSection( Column( verticalArrangement = Arrangement.spacedBy(5.dp), ) { - FilledIconButton(onClick = { - coroutineScope.launch { - logListState.scrollToItem(0) - } - }) { + val firstVisibleItem by remember { derivedStateOf { logListState.firstVisibleItemIndex } } + val layoutInfo by remember { derivedStateOf { logListState.layoutInfo } } + FilledIconButton( + onClick = { + coroutineScope.launch { + logListState.scrollToItem(0) + } + }, + enabled = firstVisibleItem != 0 + ) { Icon(Icons.Filled.KeyboardDoubleArrowUp, contentDescription = null) } - FilledIconButton(onClick = { - coroutineScope.launch { - logListState.scrollToItem((logListState.layoutInfo.totalItemsCount - 1).takeIf { it >= 0 } ?: return@launch) - } - }) { + FilledIconButton( + onClick = { + coroutineScope.launch { + logListState.scrollToItem((logListState.layoutInfo.totalItemsCount - 1).takeIf { it >= 0 } ?: return@launch) + } + }, + enabled = layoutInfo.visibleItemsInfo.lastOrNull()?.index != layoutInfo.totalItemsCount - 1 + ) { Icon(Icons.Filled.KeyboardDoubleArrowDown, contentDescription = null) } } From 8f2940e0a66bc3fb8e6ba2054ea6cafe295d32c1 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Wed, 27 Dec 2023 12:38:49 +0100 Subject: [PATCH 16/53] feat(app/scripting): warning dialog --- .../sections/scripting/ScriptsSection.kt | 42 ++++++++++++++++++- common/src/main/assets/lang/en_US.json | 4 ++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt index 1f7423c2a..ff6ea0898 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt @@ -144,7 +144,7 @@ class ScriptsSection : Section() { @Composable fun ScriptSettings(script: ModuleInfo) { - val settingsInterface = remember { + val settingsInterface = remember { val module = context.scriptManager.runtime.getModuleByName(script.name) ?: return@remember null (module.getBinding(InterfaceManager::class))?.buildInterface(EnumScriptInterface.SETTINGS) } @@ -247,6 +247,46 @@ class ScriptsSection : Section() { modifier = Modifier.align(Alignment.TopCenter) ) } + + var scriptingWarning by remember { + mutableStateOf(context.sharedPreferences.run { + getBoolean("scripting_warning", true).also { + edit().putBoolean("scripting_warning", false).apply() + } + }) + } + + if (scriptingWarning) { + var timeout by remember { + mutableIntStateOf(10) + } + + LaunchedEffect(Unit) { + while (timeout > 0) { + delay(1000) + timeout-- + } + } + + AlertDialog(onDismissRequest = { + if (timeout == 0) { + scriptingWarning = false + } + }, title = { + Text(text = context.translation["manager.dialogs.scripting_warning.title"]) + }, text = { + Text(text = context.translation["manager.dialogs.scripting_warning.content"]) + }, confirmButton = { + TextButton( + onClick = { + scriptingWarning = false + }, + enabled = timeout == 0 + ) { + Text(text = "OK " + if (timeout > 0) "($timeout)" else "") + } + }) + } } @Composable diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index ef17f75c0..79a4d63b6 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -63,6 +63,10 @@ "fetch_error": "Failed to fetch data", "category_groups": "Groups", "category_friends": "Friends" + }, + "scripting_warning": { + "title": "Warning", + "content": "SnapEnhance includes a scripting tool, allowing the execution of user-defined code on your device. Use extreme caution and only install modules from known, reliable sources. Unauthorized or unverified modules may pose security risks to your system." } } }, From ccd9c40f29ec143ca8f6770a2c7040139f73a0a0 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Wed, 27 Dec 2023 17:08:09 +0100 Subject: [PATCH 17/53] fix(core/message_logger): message serialized fields --- .../snapenhance/core/features/impl/spying/MessageLogger.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt index 0ef326c39..2bf61742d 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt @@ -143,10 +143,10 @@ class MessageLogger : Feature("MessageLogger", messageJsonObject["mMetadata"].asJsonObject.addProperty("mPlayableSnapState", "PLAYABLE") } - //serialize all properties of messageJsonObject and put in the message object + //serialize all properties of messageJsonObject and put mMessageContent & mMetadata in the message object messageInstance.javaClass.declaredFields.forEach { field -> + if (field.name != "mMessageContent" && field.name != "mMetadata") return@forEach field.isAccessible = true - if (field.name == "mDescriptor") return@forEach // prevent the client message id from being overwritten messageJsonObject[field.name]?.let { fieldValue -> field.set(messageInstance, context.gson.fromJson(fieldValue, field.type)) } From 90d76c6412e994f1b0c7cfafe89270d0ededa7be Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Wed, 27 Dec 2023 17:10:40 +0100 Subject: [PATCH 18/53] fix(core): chat message bind view event --- .../rhunk/snapenhance/core/event/events/impl/BindViewEvent.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/BindViewEvent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/BindViewEvent.kt index 5dc4ad6c1..7ea39229f 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/BindViewEvent.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/BindViewEvent.kt @@ -1,6 +1,7 @@ package me.rhunk.snapenhance.core.event.events.impl import android.view.View +import android.widget.LinearLayout import me.rhunk.snapenhance.core.event.Event class BindViewEvent( @@ -9,6 +10,7 @@ class BindViewEvent( val view: View ): Event() { inline fun chatMessage(block: (conversationId: String, messageId: String) -> Unit) { + if (view !is LinearLayout) return val modelToString = prevModel.toString() if (!modelToString.startsWith("ChatViewModel")) return modelToString.substringAfter("messageId=").substringBefore(",").split(":").apply { From 2a8fcacd2fec0aeb6246b197d6ef758745bb66c3 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Wed, 27 Dec 2023 17:39:46 +0100 Subject: [PATCH 19/53] feat(core/ui): conversation toolbox - add scripting support --- .../snapenhance/e2ee/E2EEImplementation.kt | 4 +- .../scripting/ui/EnumScriptInterface.kt | 1 + .../impl/experiments/EndToEndEncryption.kt | 93 +++++----- .../features/impl/ui/ConversationToolbox.kt | 171 ++++++++++++++++++ .../core/manager/impl/FeatureManager.kt | 1 + 5 files changed, 216 insertions(+), 54 deletions(-) create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/ConversationToolbox.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/e2ee/E2EEImplementation.kt b/app/src/main/kotlin/me/rhunk/snapenhance/e2ee/E2EEImplementation.kt index c5266d138..0456ce166 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/e2ee/E2EEImplementation.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/e2ee/E2EEImplementation.kt @@ -62,7 +62,7 @@ class E2EEImplementation ( override fun acceptPairingRequest(friendId: String, publicKey: ByteArray): ByteArray? { val kemGen = KyberKEMGenerator(secureRandom) - val encapsulatedSecret = runCatching { + val encapsulatedSecret = runCatching { kemGen.generateEncapsulated( KyberPublicKeyParameters( kyberDefaultParameters, @@ -164,7 +164,7 @@ class E2EEImplementation ( cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(encryptionKey, "AES"), IvParameterSpec(iv)) cipher.doFinal(message) }.onFailure { - context.log.error("Failed to decrypt message from $friendId", it) + context.log.warn("Failed to decrypt message for $friendId") return null }.getOrNull() } diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/EnumScriptInterface.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/EnumScriptInterface.kt index b3114ead8..6a10f02d3 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/EnumScriptInterface.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/EnumScriptInterface.kt @@ -8,4 +8,5 @@ enum class EnumScriptInterface( ) { SETTINGS("settings", BindingSide.MANAGER), FRIEND_FEED_CONTEXT_MENU("friendFeedContextMenu", BindingSide.CORE), + CONVERSATION_TOOLBOX("conversationToolbox", BindingSide.CORE), } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt index e56b7ff42..7dc1d20a7 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt @@ -7,10 +7,14 @@ import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.Shape import android.view.View import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams -import android.view.ViewGroup.MarginLayoutParams import android.widget.Button -import android.widget.TextView +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.data.MessageState import me.rhunk.snapenhance.common.data.MessagingRuleType @@ -18,14 +22,13 @@ import me.rhunk.snapenhance.common.data.RuleState import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.common.util.protobuf.ProtoWriter -import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.MessagingRuleFeature -import me.rhunk.snapenhance.core.features.impl.messaging.Messaging +import me.rhunk.snapenhance.core.features.impl.ui.ConversationToolbox import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.ui.addForegroundDrawable import me.rhunk.snapenhance.core.ui.removeForegroundDrawable @@ -157,56 +160,34 @@ class EndToEndEncryption : MessagingRuleFeature( } } - private fun openManagementPopup() { - val conversationId = context.feature(Messaging::class).openedConversationUUID?.toString() ?: return - val friendId = context.database.getDMOtherParticipant(conversationId) - - if (friendId == null) { - context.shortToast("This menu is only available in direct messages.") - return - } - - val actions = listOf( - "Initiate a new shared secret", - "Show shared key fingerprint" - ) - - ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!).apply { - setTitle("End-to-end encryption") - setItems(actions.toTypedArray()) { _, which -> - when (which) { - 0 -> { - warnKeyOverwrite(friendId) { - askForKeys(conversationId) - } - } - 1 -> { - val fingerprint = e2eeInterface.getSecretFingerprint(friendId) - ViewAppearanceHelper.newAlertDialogBuilder(context).apply { - setTitle("End-to-end encryption") - setMessage("Your fingerprint is:\n\n$fingerprint\n\nMake sure to check if it matches your friend's fingerprint!") - setPositiveButton("OK") { _, _ -> } - }.show() - } - } - } - setPositiveButton("OK") { _, _ -> } - }.show() - } - @SuppressLint("SetTextI18n", "DiscouragedApi") override fun onActivityCreate() { if (!isEnabled) return - // add button to input bar - context.event.subscribe(AddViewEvent::class) { param -> - if (param.view.toString().contains("default_input_bar")) { - (param.view as ViewGroup).addView(TextView(param.view.context).apply { - layoutParams = MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT) - setOnClickListener { openManagementPopup() } - setPadding(20, 20, 20, 20) - textSize = 23f - text = "\uD83D\uDD12" - }) + + context.feature(ConversationToolbox::class).addComposable("End-to-end Encryption", filter = { + context.database.getDMOtherParticipant(it) != null + }) { dialog, conversationId -> + val friendId = remember { + context.database.getDMOtherParticipant(conversationId) + } ?: return@addComposable + val fingerprint = remember { + runCatching { + e2eeInterface.getSecretFingerprint(friendId) + }.getOrNull() + } + if (fingerprint != null) { + Text("Your fingerprint is:\n\n$fingerprint\n\nMake sure to check if it matches your friend's fingerprint!") + } else { + Text("You don't have a shared secret with this friend yet. Click below to initiate a new one.") + } + Spacer(modifier = Modifier.height(10.dp)) + Button(onClick = { + dialog.dismiss() + warnKeyOverwrite(friendId) { + askForKeys(conversationId) + } + }) { + Text("Initiate new shared secret") } } @@ -245,6 +226,10 @@ class EndToEndEncryption : MessagingRuleFeature( viewGroup.addView(Button(context.mainActivity!!).apply { text = "Accept secret" tag = receiveSecretTag + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) setOnClickListener { handleSecretResponse(conversationId, secret) } @@ -255,6 +240,10 @@ class EndToEndEncryption : MessagingRuleFeature( viewGroup.addView(Button(context.mainActivity!!).apply { text = "Receive public key" tag = receivePublicKeyTag + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) setOnClickListener { handlePublicKeyRequest(conversationId, publicKey) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/ConversationToolbox.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/ConversationToolbox.kt new file mode 100644 index 000000000..82954b9a3 --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/ConversationToolbox.kt @@ -0,0 +1,171 @@ +package me.rhunk.snapenhance.core.features.impl.ui + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.TextView +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.times +import me.rhunk.snapenhance.common.scripting.ui.EnumScriptInterface +import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager +import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface +import me.rhunk.snapenhance.common.ui.createComposeAlertDialog +import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent +import me.rhunk.snapenhance.core.features.Feature +import me.rhunk.snapenhance.core.features.FeatureLoadParams +import me.rhunk.snapenhance.core.features.impl.messaging.Messaging +import me.rhunk.snapenhance.core.util.ktx.getId + + +data class ComposableMenu( + val title: String, + val filter: (conversationId: String) -> Boolean, + val composable: @Composable (alertDialog: AlertDialog, conversationId: String) -> Unit, +) + +class ConversationToolbox : Feature("Conversation Toolbox", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + private val composableList = mutableListOf() + private val expandedComposableCache = mutableStateMapOf() + + fun addComposable(title: String, filter: (conversationId: String) -> Boolean = { true }, composable: @Composable (alertDialog: AlertDialog, conversationId: String) -> Unit) { + composableList.add( + ComposableMenu(title, filter, composable) + ) + } + + @SuppressLint("SetTextI18n") + override fun onActivityCreate() { + val defaultInputBarId = context.resources.getId("default_input_bar") + + context.event.subscribe(AddViewEvent::class) { event -> + if (event.view.id != defaultInputBarId) return@subscribe + if (composableList.isEmpty()) return@subscribe + + (event.view as ViewGroup).addView(FrameLayout(event.view.context).apply { + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + (52 * context.resources.displayMetrics.density).toInt(), + ).apply { + gravity = Gravity.BOTTOM + } + setPadding(25, 0, 25, 0) + + addView(TextView(event.view.context).apply { + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ).apply { + gravity = Gravity.CENTER_VERTICAL + } + setOnClickListener { + openToolbox() + } + textSize = 21f + text = "\uD83E\uDDF0" + }) + }) + } + + context.scriptRuntime.eachModule { + val interfaceManager = getBinding(InterfaceManager::class)?.takeIf { + it.hasInterface(EnumScriptInterface.CONVERSATION_TOOLBOX) + } ?: return@eachModule + addComposable("\uD83D\uDCDC ${moduleInfo.displayName}") { alertDialog, conversationId -> + ScriptInterface(remember { + interfaceManager.buildInterface(EnumScriptInterface.CONVERSATION_TOOLBOX, mapOf( + "alertDialog" to alertDialog, + "conversationId" to conversationId, + )) + } ?: return@addComposable) + } + } + } + + private fun openToolbox() { + val openedConversationId = context.feature(Messaging::class).openedConversationUUID?.toString() ?: run { + context.shortToast("You must open a conversation first") + return + } + + createComposeAlertDialog(context.mainActivity!!) { alertDialog -> + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn( + min = 100.dp, + max = LocalConfiguration.current.screenHeightDp * 0.8f.dp + ) + .verticalScroll(rememberScrollState()) + ) { + Text("Conversation Toolbox", fontSize = 20.sp, modifier = Modifier + .fillMaxWidth() + .padding(10.dp), textAlign = TextAlign.Center) + Spacer(modifier = Modifier.height(10.dp)) + + composableList.reversed().forEach { (title, filter, composable) -> + if (!filter(openedConversationId)) return@forEach + Card( + modifier = Modifier + .fillMaxWidth() + .padding(5.dp), + shape = MaterialTheme.shapes.medium + ) { + Row( + modifier = Modifier + .clickable { + expandedComposableCache[title] = !(expandedComposableCache[title] ?: false) + } + .fillMaxWidth() + .padding(10.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + imageVector = if (expandedComposableCache[title] == true) Icons.Filled.KeyboardArrowDown else Icons.Filled.KeyboardArrowUp, + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), + ) + Text(title, fontSize = 16.sp, fontStyle = FontStyle.Italic) + } + if (expandedComposableCache[title] == true) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp) + ) { + runCatching { + composable(alertDialog, openedConversationId) + }.onFailure { throwable -> + Text("Failed to load composable: ${throwable.message}") + context.log.error("Failed to load composable: ${throwable.message}", throwable) + } + } + } + } + } + } + }.show() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt index 058f12562..266b0d7a5 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt @@ -118,6 +118,7 @@ class FeatureManager( EditTextOverride::class, PreventForcedLogout::class, SuspendLocationUpdates::class, + ConversationToolbox::class, ) initializeFeatures() From 7aa05e996a3e0119c6c8cbfe41718551a5bde1ba Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Wed, 27 Dec 2023 23:19:59 +0100 Subject: [PATCH 20/53] feat(scripting): messaging module --- .../snapenhance/common/data/SnapEnums.kt | 19 ++- .../features/impl/messaging/Notifications.kt | 2 +- .../core/messaging/CoreMessagingBridge.kt | 2 +- .../core/scripting/CoreScriptRuntime.kt | 2 + .../core/scripting/impl/CoreMessaging.kt | 148 ++++++++++++++++++ .../core/wrapper/impl/ConversationManager.kt | 4 +- .../core/wrapper/impl/MessageDestinations.kt | 1 - 7 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreMessaging.kt diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SnapEnums.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SnapEnums.kt index 7201ed78a..20d247f89 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SnapEnums.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SnapEnums.kt @@ -108,8 +108,23 @@ enum class MediaReferenceType { } -enum class MessageUpdate { - UNKNOWN, READ, RELEASE, SAVE, UNSAVE, ERASE, SCREENSHOT, SCREEN_RECORD, REPLAY, REACTION, REMOVEREACTION, REVOKETRANSCRIPTION, ALLOWTRANSCRIPTION, ERASESAVEDSTORYMEDIA +enum class MessageUpdate( + val key: String, +) { + UNKNOWN("unknown"), + READ("read"), + RELEASE("release"), + SAVE("save"), + UNSAVE("unsave"), + ERASE("erase"), + SCREENSHOT("screenshot"), + SCREEN_RECORD("screen_record"), + REPLAY("replay"), + REACTION("reaction"), + REMOVEREACTION("remove_reaction"), + REVOKETRANSCRIPTION("revoke_transcription"), + ALLOWTRANSCRIPTION("allow_transcription"), + ERASESAVEDSTORYMEDIA("erase_saved_story_media"), } enum class FriendLinkType(val value: Int, val shortName: String) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt index b9d23033a..7cd3c73f8 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt @@ -421,7 +421,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN context.coroutineScope.launch(coroutineDispatcher) { suspendCoroutine { continuation -> - conversationManager.fetchMessageByServerId(conversationId, serverMessageId, onSuccess = { + conversationManager.fetchMessageByServerId(conversationId, serverMessageId.toLong(), onSuccess = { if (it.senderId.toString() == context.database.myUserId) { param.invokeOriginal() continuation.resumeWith(Result.success(Unit)) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/CoreMessagingBridge.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/CoreMessagingBridge.kt index d85d09be8..2e34dbb8c 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/CoreMessagingBridge.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/CoreMessagingBridge.kt @@ -65,7 +65,7 @@ class CoreMessagingBridge( suspendCancellableCoroutine { continuation -> conversationManager?.fetchMessageByServerId( conversationId, - serverMessageId, + serverMessageId.toLong(), onSuccess = { continuation.resumeWith(Result.success(it.toBridge())) }, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt index cca660802..f7a4aa5a1 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/CoreScriptRuntime.kt @@ -7,6 +7,7 @@ import me.rhunk.snapenhance.common.scripting.ScriptRuntime import me.rhunk.snapenhance.common.scripting.bindings.BindingSide import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.scripting.impl.CoreIPC +import me.rhunk.snapenhance.core.scripting.impl.CoreMessaging import me.rhunk.snapenhance.core.scripting.impl.CoreScriptConfig import me.rhunk.snapenhance.core.scripting.impl.CoreScriptHooker @@ -23,6 +24,7 @@ class CoreScriptRuntime( CoreScriptConfig(), CoreIPC(), CoreScriptHooker(), + CoreMessaging(modContext) ) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreMessaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreMessaging.kt new file mode 100644 index 000000000..a242e14f4 --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreMessaging.kt @@ -0,0 +1,148 @@ +package me.rhunk.snapenhance.core.scripting.impl + +import me.rhunk.snapenhance.common.data.MessageUpdate +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide +import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject +import me.rhunk.snapenhance.core.ModContext +import me.rhunk.snapenhance.core.features.impl.messaging.Messaging +import me.rhunk.snapenhance.core.wrapper.impl.Message +import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import org.mozilla.javascript.Scriptable +import org.mozilla.javascript.annotations.JSFunction + +@Suppress("unused") +class CoreMessaging( + private val modContext: ModContext +) : AbstractBinding("messaging", BindingSide.CORE) { + private val conversationManager get() = modContext.feature(Messaging::class).conversationManager + + @JSFunction + fun isPresent() = conversationManager != null + + @JSFunction + fun newSnapUUID(uuid: String) = SnapUUID.fromString(uuid) + + @JSFunction + fun updateMessage( + conversationId: String, + messageId: Number, + action: String, + callback: (error: String?) -> Unit + ) { + conversationManager?.updateMessage(conversationId, messageId.toLong(), MessageUpdate.entries.find { it.key == action } + ?: throw RuntimeException("Could not find message update $action"), + callback) + } + + @JSFunction + fun fetchConversationWithMessagesPaginated( + conversationId: String, + lastMessageId: Long, + amount: Int, + callback: (error: String?, message: List) -> Unit, + ) { + conversationManager?.fetchConversationWithMessagesPaginated(conversationId, lastMessageId, amount, onSuccess = { + callback(null, it) + }, onError = { + callback(it, emptyList()) + }) + } + + @JSFunction + fun fetchConversationWithMessages( + conversationId: String, + callback: (error: String?, List) -> Unit + ) { + conversationManager?.fetchConversationWithMessages(conversationId, onSuccess = { + callback(null, it) + }, onError = { + callback(it, emptyList()) + }) + } + + @JSFunction + fun fetchMessageByServerId( + conversationId: String, + serverId: Long, + callback: (error: String?, message: Message?) -> Unit, + ) { + conversationManager?.fetchMessageByServerId(conversationId, serverId, onSuccess = { + callback(null, it) + }, onError = { + callback(it, null) + }) + } + + @JSFunction + fun fetchMessagesByServerIds( + conversationId: String, + serverIds: List, + callback: (error: String?, List) -> Unit + ) { + conversationManager?.fetchMessagesByServerIds(conversationId, serverIds.map { + it.toLong() + }, onSuccess = { + callback(null, it) + }, onError = { + callback(it, emptyList()) + }) + } + + @JSFunction + fun displayedMessages( + conversationId: String, + lastMessageId: Number, + callback: (error: String?) -> Unit + ) { + conversationManager?.displayedMessages(conversationId, lastMessageId.toLong(), callback) + } + + @JSFunction + fun fetchMessage( + conversationId: String, + messageId: Number, + callback: (error: String?, message: Message?) -> Unit + ) { + conversationManager?.fetchMessage(conversationId, messageId.toLong(), onSuccess = { + callback(null, it) + }, onError = { callback(it, null) }) + } + + @JSFunction + fun clearConversation( + conversationId: String, + callback: (error: String?) -> Unit + ) { + conversationManager?.clearConversation(conversationId, onSuccess = { + callback(null) + }, onError = { + callback(it) + }) + } + + @JSFunction + fun getOneOnOneConversationIds(userIds: List, callback: (error: String?, List) -> Unit) { + conversationManager?.getOneOnOneConversationIds(userIds, onSuccess = { + callback(null, it.map { (userId, conversationId) -> + scriptableObject { + putConst("conversationId", this, conversationId) + putConst("userId", this, userId) + } + }) + }, onError = { + callback(it, emptyList()) + }) + } + + @JSFunction + fun sendChatMessage( + conversationId: String, + message: String, + result: (error: String?) -> Unit + ) { + modContext.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), message, onSuccess = { result(null) }, onError = { result(it.toString()) }) + } + + override fun getObject() = this +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt index 730ff8acf..2f6fa863e 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt @@ -91,10 +91,10 @@ class ConversationManager( ) } - fun fetchMessageByServerId(conversationId: String, serverMessageId: String, onSuccess: (Message) -> Unit, onError: (error: String) -> Unit) { + fun fetchMessageByServerId(conversationId: String, serverMessageId: Long, onSuccess: (Message) -> Unit, onError: (error: String) -> Unit) { val serverMessageIdentifier = CallbackBuilder.createEmptyObject(context.classCache.serverMessageIdentifier.constructors.first())?.apply { setObjectField("mServerConversationId", conversationId.toSnapUUID().instanceNonNull()) - setObjectField("mServerMessageId", serverMessageId.toLong()) + setObjectField("mServerMessageId", serverMessageId) } fetchMessageByServerId.invoke( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageDestinations.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageDestinations.kt index ea45bf7fd..b6221f284 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageDestinations.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageDestinations.kt @@ -2,7 +2,6 @@ package me.rhunk.snapenhance.core.wrapper.impl import me.rhunk.snapenhance.core.wrapper.AbstractWrapper -@Suppress("UNCHECKED_CAST") class MessageDestinations(obj: Any) : AbstractWrapper(obj){ var conversations by field("mConversations", uuidArrayListMapper) var stories by field>("mStories") From b378bdde871f28fb7408c2c99e01cf195f876937 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 28 Dec 2023 00:42:01 +0100 Subject: [PATCH 21/53] feat(core): spotlight comments username --- common/src/main/assets/lang/en_US.json | 4 ++ .../snapenhance/common/config/impl/Global.kt | 1 + .../core/features/impl/messaging/Messaging.kt | 21 ++++++++ .../impl/ui/SpotlightCommentsUsername.kt | 54 +++++++++++++++++++ .../core/manager/impl/FeatureManager.kt | 1 + .../core/wrapper/impl/Snapchatter.kt | 19 +++++++ 6 files changed, 100 insertions(+) create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SpotlightCommentsUsername.kt create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/Snapchatter.kt diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 79a4d63b6..7287ae4c8 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -497,6 +497,10 @@ "name": "Block Ads", "description": "Prevents Advertisements from being displayed" }, + "spotlight_comments_username": { + "name": "Spotlight Comments Username", + "description": "Shows author username in Spotlight comments" + }, "bypass_video_length_restriction": { "name": "Bypass Video Length Restrictions", "description": "Single: sends a single video\nSplit: split videos after editing" diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt index c5f7143bd..c74d00338 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt @@ -14,6 +14,7 @@ class Global : ConfigContainer() { val disableMetrics = boolean("disable_metrics") val disablePublicStories = boolean("disable_public_stories") { requireRestart(); requireCleanCache() } val blockAds = boolean("block_ads") + val spotlightCommentsUsername = boolean("spotlight_comments_username") { requireRestart() } val bypassVideoLengthRestriction = unique("bypass_video_length_restriction", "split", "single") { addNotices( FeatureNotice.BAN_RISK); requireRestart(); nativeHooks() } val disableGooglePlayDialogs = boolean("disable_google_play_dialogs") { requireRestart() } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt index ef495bd72..19bee0215 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt @@ -16,12 +16,16 @@ import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull import me.rhunk.snapenhance.core.wrapper.impl.ConversationManager import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.core.wrapper.impl.Snapchatter import me.rhunk.snapenhance.core.wrapper.impl.toSnapUUID +import java.util.concurrent.Future class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) { var conversationManager: ConversationManager? = null private set private var conversationManagerDelegate: Any? = null + private var identityDelegate: Any? = null + var openedConversationUUID: SnapUUID? = null private set var lastFetchConversationUserUUID: SnapUUID? = null @@ -57,6 +61,12 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } } } + + context.mappings.getMappedClass("callbacks", "IdentityDelegate").apply { + hookConstructor(HookStage.AFTER) { + identityDelegate = it.thisObject() + } + } } fun getFeedCachedMessageIds(conversationId: String) = feedCachedSnapMessages[conversationId] @@ -169,4 +179,15 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C it.setResult(null) } } + + fun fetchSnapchatterInfos(userIds: List): List { + val identity = identityDelegate ?: return emptyList() + val future = identity::class.java.methods.first { + it.name == "fetchSnapchatterInfos" + }.invoke(identity, userIds.map { + it.toSnapUUID().instanceNonNull() + }) as Future<*> + + return (future.get() as? List<*>)?.map { Snapchatter(it) } ?: return emptyList() + } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SpotlightCommentsUsername.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SpotlightCommentsUsername.kt new file mode 100644 index 000000000..25892874c --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SpotlightCommentsUsername.kt @@ -0,0 +1,54 @@ +package me.rhunk.snapenhance.core.features.impl.ui + +import android.annotation.SuppressLint +import android.widget.TextView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent +import me.rhunk.snapenhance.core.features.Feature +import me.rhunk.snapenhance.core.features.FeatureLoadParams +import me.rhunk.snapenhance.core.features.impl.messaging.Messaging +import me.rhunk.snapenhance.core.util.EvictingMap +import me.rhunk.snapenhance.core.util.ktx.getId + +class SpotlightCommentsUsername : Feature("SpotlightCommentsUsername", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + private val usernameCache = EvictingMap(150) + + @SuppressLint("SetTextI18n") + override fun onActivityCreate() { + if (!context.config.global.spotlightCommentsUsername.get()) return + + val messaging = context.feature(Messaging::class) + val commentsCreatorBadgeTimestampId = context.resources.getId("comments_creator_badge_timestamp") + + context.event.subscribe(BindViewEvent::class) { event -> + val commentsCreatorBadgeTimestamp = event.view.findViewById(commentsCreatorBadgeTimestampId) ?: return@subscribe + + val posterUserId = event.prevModel.toString().takeIf { it.startsWith("Comment") } + ?.substringAfter("posterUserId=")?.substringBefore(",")?.substringBefore(")") ?: return@subscribe + + fun setUsername(username: String) { + usernameCache[posterUserId] = username + commentsCreatorBadgeTimestamp.text = " (${username})" + commentsCreatorBadgeTimestamp.text.toString() + } + + usernameCache[posterUserId]?.let { + setUsername(it) + return@subscribe + } + + context.coroutineScope.launch { + val username = runCatching { + messaging.fetchSnapchatterInfos(listOf(posterUserId)).firstOrNull() + }.onFailure { + context.log.error("Failed to fetch snapchatter info for user $posterUserId", it) + }.getOrNull()?.username ?: return@launch + + withContext(Dispatchers.Main) { + setUsername(username) + } + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt index 266b0d7a5..6e0df5101 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt @@ -119,6 +119,7 @@ class FeatureManager( PreventForcedLogout::class, SuspendLocationUpdates::class, ConversationToolbox::class, + SpotlightCommentsUsername::class, ) initializeFeatures() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/Snapchatter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/Snapchatter.kt new file mode 100644 index 000000000..0a3dcca1c --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/Snapchatter.kt @@ -0,0 +1,19 @@ +package me.rhunk.snapenhance.core.wrapper.impl + +import me.rhunk.snapenhance.core.wrapper.AbstractWrapper + + + +class BitmojiInfo(obj: Any?) : AbstractWrapper(obj) { + var avatarId by field("mAvatarId") + var backgroundId by field("mBackgroundId") + var sceneId by field("mSceneId") + var selfieId by field("mSelfieId") +} + +class Snapchatter(obj: Any?) : AbstractWrapper(obj) { + val bitmojiInfo by field("mBitmojiInfo") + var displayName by field("mDisplayName") + var userId by field("mUserId") { SnapUUID(it) } + var username by field("mUsername") +} \ No newline at end of file From 8f06688f55ef2aef6178d08c7a21722fbfef7db8 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 28 Dec 2023 16:55:30 +0100 Subject: [PATCH 22/53] fea(scripting/messaging): snapchatter info --- .../snapenhance/core/scripting/impl/CoreMessaging.kt | 11 ++++++++++- .../snapenhance/core/wrapper/impl/Snapchatter.kt | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreMessaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreMessaging.kt index a242e14f4..8e186b6df 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreMessaging.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/scripting/impl/CoreMessaging.kt @@ -8,6 +8,7 @@ import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.core.wrapper.impl.Snapchatter import org.mozilla.javascript.Scriptable import org.mozilla.javascript.annotations.JSFunction @@ -15,7 +16,8 @@ import org.mozilla.javascript.annotations.JSFunction class CoreMessaging( private val modContext: ModContext ) : AbstractBinding("messaging", BindingSide.CORE) { - private val conversationManager get() = modContext.feature(Messaging::class).conversationManager + private val messaging by lazy { modContext.feature(Messaging::class) } + private val conversationManager get() = messaging.conversationManager @JSFunction fun isPresent() = conversationManager != null @@ -144,5 +146,12 @@ class CoreMessaging( modContext.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), message, onSuccess = { result(null) }, onError = { result(it.toString()) }) } + @JSFunction + fun fetchSnapchatterInfos( + userIds: List + ): List { + return messaging.fetchSnapchatterInfos(userIds = userIds) + } + override fun getObject() = this } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/Snapchatter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/Snapchatter.kt index 0a3dcca1c..38619c59c 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/Snapchatter.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/Snapchatter.kt @@ -1,19 +1,28 @@ package me.rhunk.snapenhance.core.wrapper.impl import me.rhunk.snapenhance.core.wrapper.AbstractWrapper - +import org.mozilla.javascript.annotations.JSGetter +import org.mozilla.javascript.annotations.JSSetter class BitmojiInfo(obj: Any?) : AbstractWrapper(obj) { + @get:JSGetter @set:JSSetter var avatarId by field("mAvatarId") + @get:JSGetter @set:JSSetter var backgroundId by field("mBackgroundId") + @get:JSGetter @set:JSSetter var sceneId by field("mSceneId") + @get:JSGetter @set:JSSetter var selfieId by field("mSelfieId") } class Snapchatter(obj: Any?) : AbstractWrapper(obj) { + @get:JSGetter val bitmojiInfo by field("mBitmojiInfo") + @get:JSGetter @set:JSSetter var displayName by field("mDisplayName") + @get:JSGetter @set:JSSetter var userId by field("mUserId") { SnapUUID(it) } + @get:JSGetter @set:JSSetter var username by field("mUsername") } \ No newline at end of file From d7d3834f215bc28dd82bb58f2b8a19e40cef8de8 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 28 Dec 2023 23:11:17 +0100 Subject: [PATCH 23/53] fea(scripting): networking --- .../snapenhance/common/scripting/JSModule.kt | 2 + .../common/scripting/impl/Networking.kt | 162 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/Networking.kt diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt index 0417bdea8..bba37b19e 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt @@ -4,6 +4,7 @@ import android.os.Handler import android.widget.Toast import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding import me.rhunk.snapenhance.common.scripting.bindings.BindingsContext +import me.rhunk.snapenhance.common.scripting.impl.Networking import me.rhunk.snapenhance.common.scripting.impl.JavaInterfaces import me.rhunk.snapenhance.common.scripting.ktx.contextScope import me.rhunk.snapenhance.common.scripting.ktx.putFunction @@ -55,6 +56,7 @@ class JSModule( registerBindings( JavaInterfaces(), InterfaceManager(), + Networking(), ) moduleObject.putFunction("setField") { args -> diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/Networking.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/Networking.kt new file mode 100644 index 000000000..2116fe46e --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/Networking.kt @@ -0,0 +1,162 @@ +package me.rhunk.snapenhance.common.scripting.impl + +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide +import me.rhunk.snapenhance.common.scripting.ktx.contextScope +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okio.ByteString +import okio.ByteString.Companion.toByteString +import org.mozilla.javascript.Function +import org.mozilla.javascript.Scriptable +import org.mozilla.javascript.annotations.JSFunction +import org.mozilla.javascript.annotations.JSGetter + + +class Networking : AbstractBinding("networking", BindingSide.COMMON) { + private val defaultHttpClient = OkHttpClient() + + inner class RequestBuilderWrapper( + val requestBuilder: Request.Builder + ) { + @JSFunction + fun url(url: String) = requestBuilder.url(url).let { this } + + @JSFunction + fun addHeader(name: String, value: String) = requestBuilder.addHeader(name, value).let { this } + + @JSFunction + fun removeHeader(name: String) = requestBuilder.removeHeader(name).let { this } + + @JSFunction + fun method(method: String, body: String) = requestBuilder.method(method, okhttp3.RequestBody.create(null, body)).let { this } + + @JSFunction + fun method(method: String, body: java.io.InputStream) = requestBuilder.method(method, okhttp3.RequestBody.create(null, body.readBytes())).let { this } + + @JSFunction + fun method(method: String, body: ByteArray) = requestBuilder.method(method, okhttp3.RequestBody.create(null, body)).let { this } + } + + inner class ResponseWrapper( + private val response: Response + ) { + @get:JSGetter + val statusCode get() = response.code + @get:JSGetter + val statusMessage get() = response.message + @get:JSGetter + val headers get() = response.headers.toMultimap().mapValues { it.value.joinToString(", ") } + @get:JSGetter + val bodyAsString get() = response.body.string() + @get:JSGetter + val bodyAsStream get() = response.body.byteStream() + @get:JSGetter + val bodyAsByteArray get() = response.body.bytes() + @get:JSGetter + val contentLength get() = response.body.contentLength() + @JSFunction fun getHeader(name: String) = response.header(name) + @JSFunction fun close() = response.close() + } + + inner class WebsocketWrapper( + private val websocket: WebSocket + ) { + @JSFunction fun cancel() = websocket.cancel() + @JSFunction fun close(code: Int, reason: String) = websocket.close(code, reason) + @JSFunction fun queueSize() = websocket.queueSize() + @JSFunction fun send(bytes: ByteArray) = websocket.send(bytes.toByteString()) + @JSFunction fun send(text: String) = websocket.send(text) + } + + @JSFunction + fun getUrl(url: String, callback: (error: String?, response: String) -> Unit) { + defaultHttpClient.newCall(Request.Builder().url(url).build()).enqueue(object : okhttp3.Callback { + override fun onFailure(call: okhttp3.Call, e: java.io.IOException) { + callback(e.message, "") + } + + override fun onResponse(call: okhttp3.Call, response: Response) { + response.use { + callback(null, it.body.string()) + } + } + }) + } + + @JSFunction + fun getUrlAsStream(url: String, callback: (error: String?, response: java.io.InputStream) -> Unit) { + defaultHttpClient.newCall(Request.Builder().url(url).build()).enqueue(object : okhttp3.Callback { + override fun onFailure(call: okhttp3.Call, e: java.io.IOException) { + callback(e.message, java.io.ByteArrayInputStream(byteArrayOf())) + } + + override fun onResponse(call: okhttp3.Call, response: Response) { + response.use { + callback(null, it.body.byteStream()) + } + } + }) + } + + @JSFunction + fun newRequest() = RequestBuilderWrapper(Request.Builder()) + + @JSFunction + fun newWebSocket(requestBuilder: RequestBuilderWrapper, listener: Scriptable): WebsocketWrapper { + return defaultHttpClient.newWebSocket(requestBuilder.requestBuilder.build(), object: WebSocketListener() { + private fun callListener(name: String, websocket: WebSocket, vararg args: Any?) { + contextScope { + (listener.get(name, listener) as? Function)?.call(this, listener, listener, arrayOf(WebsocketWrapper(websocket), *args)) + } + } + + override fun onOpen(webSocket: WebSocket, response: Response) { + callListener("onOpen", webSocket, ResponseWrapper(response)) + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + callListener("onClosed", webSocket, code, reason) + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + callListener("onClosing", webSocket, code, reason) + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + callListener("onFailure", webSocket, t.message, response?.let { ResponseWrapper(it) }) + } + + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + callListener("onMessageBytes", webSocket, bytes.toByteArray()) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + callListener("onMessageText", webSocket, text) + } + }).let { WebsocketWrapper(it) } + } + + @JSFunction + fun enqueue(requestBuilder: RequestBuilderWrapper, callback: (error: String?, response: ResponseWrapper?) -> Unit) { + defaultHttpClient.newCall(requestBuilder.requestBuilder.build()).enqueue(object : okhttp3.Callback { + override fun onFailure(call: okhttp3.Call, e: java.io.IOException) { + callback(e.message, null) + } + + override fun onResponse(call: okhttp3.Call, response: Response) { + response.use { + callback(null, ResponseWrapper(it)) + } + } + }) + } + + @JSFunction + fun execute(requestBuilder: RequestBuilderWrapper) = ResponseWrapper(defaultHttpClient.newCall(requestBuilder.requestBuilder.build()).execute()) + + override fun getObject() = this +} \ No newline at end of file From ffd42de7f1f1370425ea051f4602588065ae83fb Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Fri, 29 Dec 2023 01:01:28 +0100 Subject: [PATCH 24/53] fix(core): SuspendLocationUpdates --- .../impl/global/SuspendLocationUpdates.kt | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt index 767a13e3c..292105348 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt @@ -10,6 +10,7 @@ import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getId +import java.util.WeakHashMap //TODO: bridge shared preferences class SuspendLocationUpdates : BridgeFileFeature( @@ -17,15 +18,39 @@ class SuspendLocationUpdates : BridgeFileFeature( loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC, bridgeFileType = BridgeFileType.SUSPEND_LOCATION_STATE ) { + private val streamSendHandlerInstanceMap = WeakHashMap Unit>() private val isEnabled get() = context.config.global.suspendLocationUpdates.get() + override fun init() { if (!isEnabled) return reload() - context.classCache.unifiedGrpcService.hook("bidiStreamingCall", HookStage.BEFORE) { param -> - val uri = param.arg(0) - if (uri == "/snapchat.valis.Valis/Communicate" && exists("true")) { - param.setResult(null) + findClass("com.snapchat.client.grpc.ClientStreamSendHandler\$CppProxy").hook("send", HookStage.BEFORE) { param -> + if (param.nullableThisObject() !in streamSendHandlerInstanceMap) return@hook + if (!exists("true")) return@hook + param.setResult(null) + } + + context.classCache.unifiedGrpcService.apply { + hook("unaryCall", HookStage.BEFORE) { param -> + val uri = param.arg(0) + if (exists("true") && uri == "/snapchat.valis.Valis/SendClientUpdate") { + param.setResult(null) + } + } + + hook("bidiStreamingCall", HookStage.AFTER) { param -> + val uri = param.arg(0) + if (uri != "/snapchat.valis.Valis/Communicate") return@hook + param.getResult()?.let { instance -> + streamSendHandlerInstanceMap[instance] = { + runCatching { + instance::class.java.methods.first { it.name == "closeStream" }.invoke(instance) + }.onFailure { + context.log.error("Failed to close stream send handler instance", it) + } + } + } } } } @@ -47,6 +72,12 @@ class SuspendLocationUpdates : BridgeFileFeature( ViewGroup.LayoutParams.WRAP_CONTENT ) setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + streamSendHandlerInstanceMap.entries.removeIf { (_, closeStream) -> + closeStream() + true + } + } setState("true", isChecked) } }) From a7f4f1cdafc07f7a49f0d2aba56343dfc5f13455 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sat, 30 Dec 2023 00:46:06 +0100 Subject: [PATCH 25/53] feat(app/manifest): set MainActivity default --- app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a541905d8..07f706f70 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,6 +44,7 @@ + Date: Sat, 30 Dec 2023 16:30:05 +0100 Subject: [PATCH 26/53] feat(app/tasks): merge videos --- .../snapenhance/download/DownloadProcessor.kt | 32 +- .../snapenhance/download/FFMpegProcessor.kt | 103 +++++-- .../me/rhunk/snapenhance/task/PendingTask.kt | 3 +- .../me/rhunk/snapenhance/task/TaskManager.kt | 25 +- .../ui/manager/sections/TasksSection.kt | 282 ++++++++++++++++-- .../manager/sections/social/LoggedStories.kt | 4 +- common/src/main/assets/lang/en_US.json | 16 +- .../rhunk/snapenhance/common/data/FileType.kt | 5 + .../common/data/download/DownloadRequest.kt | 4 +- .../data/download/MediaDownloadSource.kt | 24 +- .../impl/downloader/MediaDownloader.kt | 2 +- 11 files changed, 406 insertions(+), 94 deletions(-) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt index 3d329d146..bf2a89a75 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt @@ -87,27 +87,15 @@ class DownloadProcessor ( fallbackToast(it) } - private fun newFFMpegProcessor(pendingTask: PendingTask) = FFMpegProcessor( - logManager = remoteSideContext.log, - ffmpegOptions = remoteSideContext.config.root.downloader.ffmpegOptions, - onStatistics = { - pendingTask.updateProgress("Processing (frames=${it.videoFrameNumber}, fps=${it.videoFps}, time=${it.time}, bitrate=${it.bitrate}, speed=${it.speed})") - } - ) + private fun newFFMpegProcessor(pendingTask: PendingTask) = FFMpegProcessor.newFFMpegProcessor(remoteSideContext, pendingTask) @SuppressLint("UnspecifiedRegisterReceiverFlag") - private suspend fun saveMediaToGallery(pendingTask: PendingTask, inputFile: File, metadata: DownloadMetadata) { + suspend fun saveMediaToGallery(pendingTask: PendingTask, inputFile: File, metadata: DownloadMetadata) { if (coroutineContext.job.isCancelled) return runCatching { var fileType = FileType.fromFile(inputFile) - if (fileType == FileType.UNKNOWN) { - callbackOnFailure(translation.format("failed_gallery_toast", "error" to "Unknown media type"), null) - pendingTask.fail("Unknown media type") - return - } - if (fileType.isImage) { remoteSideContext.config.root.downloader.forceImageFormat.getNullable()?.let { format -> val bitmap = BitmapFactory.decodeFile(inputFile.absolutePath) ?: throw Exception("Failed to decode bitmap") @@ -154,9 +142,9 @@ class DownloadProcessor ( pendingTask.success() runCatching { - val mediaScanIntent = Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE") - mediaScanIntent.setData(outputFile.uri) - remoteSideContext.androidContext.sendBroadcast(mediaScanIntent) + remoteSideContext.androidContext.sendBroadcast(Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE").apply { + data = outputFile.uri + }) }.onFailure { remoteSideContext.log.error("Failed to scan media file", it) callbackOnFailure(translation.format("failed_gallery_toast", "error" to it.toString()), it.message) @@ -266,7 +254,7 @@ class DownloadProcessor ( val outputFile = File.createTempFile("voice_note", ".$format") newFFMpegProcessor(pendingTask).execute(FFMpegProcessor.Request( action = FFMpegProcessor.Action.AUDIO_CONVERSION, - input = media.file, + inputs = listOf(media.file), output = outputFile )) media.file.delete() @@ -303,7 +291,7 @@ class DownloadProcessor ( runCatching { newFFMpegProcessor(pendingTask).execute(FFMpegProcessor.Request( action = FFMpegProcessor.Action.DOWNLOAD_DASH, - input = dashPlaylistFile, + inputs = listOf(dashPlaylistFile), output = outputFile, startTime = dashOptions.offsetTime, duration = dashOptions.duration @@ -356,7 +344,8 @@ class DownloadProcessor ( val pendingTask = remoteSideContext.taskManager.createPendingTask( Task( type = TaskType.DOWNLOAD, - title = downloadMetadata.downloadSource + " (" + downloadMetadata.mediaAuthor + ")", + title = downloadMetadata.downloadSource, + author = downloadMetadata.mediaAuthor, hash = downloadMetadata.mediaIdentifier ) ).apply { @@ -406,7 +395,6 @@ class DownloadProcessor ( if (shouldMergeOverlay) { assert(downloadedMedias.size == 2) - //TODO: convert "mp4 images" into real images val media = downloadedMedias.entries.first { !it.key.isOverlay }.value val overlayMedia = downloadedMedias.entries.first { it.key.isOverlay }.value @@ -418,7 +406,7 @@ class DownloadProcessor ( newFFMpegProcessor(pendingTask).execute(FFMpegProcessor.Request( action = FFMpegProcessor.Action.MERGE_OVERLAY, - input = renamedMedia, + inputs = listOf(renamedMedia), output = mergedOverlay, overlay = renamedOverlayMedia )) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/FFMpegProcessor.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/FFMpegProcessor.kt index 58ad1988a..1328edfb4 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/download/FFMpegProcessor.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/FFMpegProcessor.kt @@ -1,35 +1,43 @@ package me.rhunk.snapenhance.download +import android.media.MediaMetadataRetriever import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.FFmpegSession import com.arthenica.ffmpegkit.Level import com.arthenica.ffmpegkit.Statistics import kotlinx.coroutines.suspendCancellableCoroutine import me.rhunk.snapenhance.LogManager +import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.common.config.impl.DownloaderConfig import me.rhunk.snapenhance.common.logger.LogLevel +import me.rhunk.snapenhance.task.PendingTask import java.io.File import java.util.concurrent.Executors -class ArgumentList : LinkedHashMap>() { +class ArgumentList { + private val arguments = mutableListOf>() + operator fun plusAssign(stringPair: Pair) { - val (key, value) = stringPair - if (this.containsKey(key)) { - this[key]!!.add(value) - } else { - this[key] = mutableListOf(value) - } + arguments += stringPair } operator fun plusAssign(key: String) { - this[key] = mutableListOf().apply { - this += "" - } + arguments += key to "" } operator fun minusAssign(key: String) { - this.remove(key) + arguments.removeIf { it.first == key } + } + + operator fun get(key: String) = arguments.find { it.first == key }?.second + + fun forEach(action: (Pair) -> Unit) { + arguments.forEach(action) + } + + fun clear() { + arguments.clear() } } @@ -41,16 +49,25 @@ class FFMpegProcessor( ) { companion object { private const val TAG = "ffmpeg-processor" + + fun newFFMpegProcessor(context: RemoteSideContext, pendingTask: PendingTask) = FFMpegProcessor( + logManager = context.log, + ffmpegOptions = context.config.root.downloader.ffmpegOptions, + onStatistics = { + pendingTask.updateProgress("Processing (frames=${it.videoFrameNumber}, fps=${it.videoFps}, time=${it.time}, bitrate=${it.bitrate}, speed=${it.speed})") + } + ) } enum class Action { DOWNLOAD_DASH, MERGE_OVERLAY, AUDIO_CONVERSION, + MERGE_MEDIA } data class Request( val action: Action, - val input: File, + val inputs: List, val output: File, val overlay: File? = null, //only for MERGE_OVERLAY val startTime: Long? = null, //only for DOWNLOAD_DASH @@ -61,14 +78,8 @@ class FFMpegProcessor( private suspend fun newFFMpegTask(globalArguments: ArgumentList, inputArguments: ArgumentList, outputArguments: ArgumentList) = suspendCancellableCoroutine { val stringBuilder = StringBuilder() arrayOf(globalArguments, inputArguments, outputArguments).forEach { argumentList -> - argumentList.forEach { (key, values) -> - values.forEach valueForEach@{ value -> - if (value.isEmpty()) { - stringBuilder.append("$key ") - return@valueForEach - } - stringBuilder.append("$key $value ") - } + argumentList.forEach { (key, value) -> + stringBuilder.append("$key ${value.takeIf { it.isNotEmpty() }?.plus(" ") ?: ""}") } } @@ -102,7 +113,9 @@ class FFMpegProcessor( } val inputArguments = ArgumentList().apply { - this += "-i" to args.input.absolutePath + args.inputs.forEach { file -> + this += "-i" to file.absolutePath + } } val outputArguments = ArgumentList().apply { @@ -133,6 +146,54 @@ class FFMpegProcessor( outputArguments -= "-c:v" } } + Action.MERGE_MEDIA -> { + inputArguments.clear() + val filesInfo = args.inputs.mapNotNull { file -> + runCatching { + MediaMetadataRetriever().apply { setDataSource(file.absolutePath) } + }.getOrNull()?.let { file to it } + } + + val (maxWidth, maxHeight) = filesInfo.maxByOrNull { (_, r) -> + r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: 0 + }?.let { (_, r) -> + r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() to + r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() + } ?: throw Exception("Failed to get video size") + + val filterFirstPart = StringBuilder() + val filterSecondPart = StringBuilder() + var containsNoSound = false + + filesInfo.forEachIndexed { index, (file, retriever) -> + filterFirstPart.append("[$index:v]scale=$maxWidth:$maxHeight,setsar=1[v$index];") + if (retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) == "yes") { + filterSecondPart.append("[v$index][$index:a]") + } else { + containsNoSound = true + filterSecondPart.append("[v$index][${filesInfo.size}]") + } + inputArguments += "-i" to file.absolutePath + } + + if (containsNoSound) { + inputArguments += "-f" to "lavfi" + inputArguments += "-t" to "0.1" + inputArguments += "-i" to "anullsrc=channel_layout=stereo:sample_rate=44100" + } + + if (outputArguments["-c:a"] == "copy") { + outputArguments -= "-c:a" + } + + outputArguments += "-fps_mode" to "vfr" + + outputArguments += "-filter_complex" to "\"$filterFirstPart ${filterSecondPart}concat=n=${args.inputs.size}:v=1:a=1[vout][aout]\"" + outputArguments += "-map" to "\"[aout]\"" + outputArguments += "-map" to "\"[vout]\"" + + filesInfo.forEach { it.second.close() } + } } outputArguments += args.output.absolutePath newFFMpegTask(globalArguments, inputArguments, outputArguments) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/task/PendingTask.kt b/app/src/main/kotlin/me/rhunk/snapenhance/task/PendingTask.kt index 1c7f8d66e..63c94080e 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/task/PendingTask.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/task/PendingTask.kt @@ -44,6 +44,7 @@ data class PendingTaskListener( data class Task( val type: TaskType, val title: String, + val author: String?, val hash: String ) { var changeListener: () -> Unit = {} @@ -106,7 +107,7 @@ class PendingTask( } fun updateProgress(label: String, progress: Int = -1) { - _progress = progress + _progress = progress.coerceIn(-1, 100) progressLabel = label } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/task/TaskManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/task/TaskManager.kt index 0b85b6fed..990f318a1 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/task/TaskManager.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/task/TaskManager.kt @@ -26,6 +26,7 @@ class TaskManager( "id INTEGER PRIMARY KEY AUTOINCREMENT", "hash VARCHAR UNIQUE", "title VARCHAR(255) NOT NULL", + "author VARCHAR(255)", "type VARCHAR(255) NOT NULL", "status VARCHAR(255) NOT NULL", "extra TEXT" @@ -37,7 +38,12 @@ class TaskManager( private val activeTasks = mutableMapOf() private fun readTaskFromCursor(cursor: android.database.Cursor): Task { - val task = Task(TaskType.fromKey(cursor.getStringOrNull("type")!!), cursor.getStringOrNull("title")!!, cursor.getStringOrNull("hash")!!) + val task = Task( + type = TaskType.fromKey(cursor.getStringOrNull("type")!!), + title = cursor.getStringOrNull("title")!!, + author = cursor.getStringOrNull("author"), + hash = cursor.getStringOrNull("hash")!! + ) task.status = TaskStatus.fromKey(cursor.getStringOrNull("status")!!) task.extra = cursor.getStringOrNull("extra") task.changeListener = { @@ -60,6 +66,7 @@ class TaskManager( val result = taskDatabase.insert("tasks", null, ContentValues().apply { put("type", task.type.key) put("hash", task.hash) + put("author", task.author) put("title", task.title) put("status", task.status.key) put("extra", task.extra) @@ -91,6 +98,22 @@ class TaskManager( } } + fun removeTask(task: Task) { + runBlocking { + activeTasks.entries.find { it.value.task == task }?.let { + activeTasks.remove(it.key) + runCatching { + it.value.cancel() + }.onFailure { + remoteSideContext.log.warn("Failed to cancel task ${task.hash}") + } + } + launch(queueExecutor.asCoroutineDispatcher()) { + taskDatabase.execSQL("DELETE FROM tasks WHERE hash = ?", arrayOf(task.hash)) + } + } + } + fun createPendingTask(task: Task): PendingTask { val taskId = putNewTask(task) task.changeListener = { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/TasksSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/TasksSection.kt index 0070875f7..d03a8aa01 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/TasksSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/TasksSection.kt @@ -1,5 +1,8 @@ package me.rhunk.snapenhance.ui.manager.sections + import android.content.Intent +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -10,12 +13,23 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.Lifecycle +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import me.rhunk.snapenhance.bridge.DownloadCallback +import me.rhunk.snapenhance.common.data.download.DownloadMetadata +import me.rhunk.snapenhance.common.data.download.MediaDownloadSource +import me.rhunk.snapenhance.common.data.download.createNewFilePath +import me.rhunk.snapenhance.common.util.ktx.longHashCode +import me.rhunk.snapenhance.download.DownloadProcessor +import me.rhunk.snapenhance.download.FFMpegProcessor import me.rhunk.snapenhance.task.PendingTask import me.rhunk.snapenhance.task.PendingTaskListener import me.rhunk.snapenhance.task.Task @@ -23,41 +37,201 @@ import me.rhunk.snapenhance.task.TaskStatus import me.rhunk.snapenhance.task.TaskType import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.util.OnLifecycleEvent +import java.io.File +import java.util.UUID +import kotlin.math.absoluteValue class TasksSection : Section() { private var activeTasks by mutableStateOf(listOf()) private lateinit var recentTasks: MutableList + private val taskSelection = mutableStateListOf>() + + private fun fetchActiveTasks(scope: CoroutineScope = context.coroutineScope) { + scope.launch(Dispatchers.IO) { + activeTasks = context.taskManager.getActiveTasks().values.sortedByDescending { it.taskId }.toMutableList() + } + } + + private fun mergeSelection(selection: List>) { + val firstTask = selection.first().first + + val taskHash = UUID.randomUUID().toString().longHashCode().absoluteValue.toString(16) + val pendingTask = context.taskManager.createPendingTask( + Task(TaskType.DOWNLOAD, "Merge ${selection.size} files", firstTask.author, taskHash) + ) + pendingTask.status = TaskStatus.RUNNING + fetchActiveTasks() + + context.coroutineScope.launch { + val filesToMerge = mutableListOf() + + selection.forEach { (task, documentFile) -> + val tempFile = File.createTempFile(task.hash, "." + documentFile.name?.substringAfterLast("."), context.androidContext.cacheDir).also { + it.deleteOnExit() + } + + runCatching { + pendingTask.updateProgress("Copying ${documentFile.name}") + context.androidContext.contentResolver.openInputStream(documentFile.uri)?.use { inputStream -> + //copy with progress + val length = documentFile.length().toFloat() + tempFile.outputStream().use { outputStream -> + val buffer = ByteArray(16 * 1024) + var read: Int + while (inputStream.read(buffer).also { read = it } != -1) { + outputStream.write(buffer, 0, read) + pendingTask.updateProgress("Copying ${documentFile.name}", (outputStream.channel.position().toFloat() / length * 100f).toInt()) + } + outputStream.flush() + filesToMerge.add(tempFile) + } + } + }.onFailure { + pendingTask.fail("Failed to copy file $documentFile to $tempFile") + filesToMerge.forEach { it.delete() } + return@launch + } + } + + val mergedFile = File.createTempFile("merged", ".mp4", context.androidContext.cacheDir).also { + it.deleteOnExit() + } + + runCatching { + context.shortToast("Merging ${filesToMerge.size} files") + FFMpegProcessor.newFFMpegProcessor(context, pendingTask).execute( + FFMpegProcessor.Request(FFMpegProcessor.Action.MERGE_MEDIA, filesToMerge, mergedFile) + ) + DownloadProcessor(context, object: DownloadCallback.Default() { + override fun onSuccess(outputPath: String) { + context.log.verbose("Merged files to $outputPath") + } + }).saveMediaToGallery(pendingTask, mergedFile, DownloadMetadata( + mediaIdentifier = taskHash, + outputPath = createNewFilePath( + context.config.root, + taskHash, + downloadSource = MediaDownloadSource.MERGED, + mediaAuthor = firstTask.author, + creationTimestamp = System.currentTimeMillis() + ), + mediaAuthor = firstTask.author, + downloadSource = MediaDownloadSource.MERGED.translate(context.translation), + iconUrl = null + )) + }.onFailure { + context.log.error("Failed to merge files", it) + pendingTask.fail(it.message ?: "Failed to merge files") + }.onSuccess { + pendingTask.success() + } + filesToMerge.forEach { it.delete() } + mergedFile.delete() + }.also { + pendingTask.addListener(PendingTaskListener(onCancel = { it.cancel() })) + } + } + @Composable override fun TopBarActions(rowScope: RowScope) { var showConfirmDialog by remember { mutableStateOf(false) } + if (taskSelection.size == 1 && taskSelection.firstOrNull()?.second?.exists() == true) { + taskSelection.firstOrNull()?.second?.takeIf { it.exists() }?.let { documentFile -> + IconButton(onClick = { + runCatching { + context.androidContext.startActivity(Intent(Intent.ACTION_VIEW).apply { + setDataAndType(documentFile.uri, documentFile.type) + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK + }) + taskSelection.clear() + }.onFailure { + context.log.error("Failed to open file ${taskSelection.first().second}", it) + } + }) { + Icon(Icons.Filled.OpenInNew, contentDescription = "Open") + } + } + } + + if (taskSelection.size > 1 && taskSelection.all { it.second?.type?.contains("video") == true }) { + IconButton(onClick = { + mergeSelection(taskSelection.toList().also { + taskSelection.clear() + }.map { it.first to it.second!! }) + }) { + Icon(Icons.Filled.Merge, contentDescription = "Merge") + } + } + IconButton(onClick = { showConfirmDialog = true }) { - Icon(Icons.Filled.Delete, contentDescription = "Clear all tasks") + Icon(Icons.Filled.Delete, contentDescription = "Clear tasks") } if (showConfirmDialog) { + var alsoDeleteFiles by remember { mutableStateOf(false) } + AlertDialog( onDismissRequest = { showConfirmDialog = false }, - title = { Text("Clear all tasks") }, - text = { Text("Are you sure you want to clear all tasks?") }, + title = { + if (taskSelection.isNotEmpty()) { + Text("Remove ${taskSelection.size} tasks?") + } else { + Text("Remove all tasks?") + } + }, + text = { + Column { + if (taskSelection.isNotEmpty()) { + Text("Are you sure you want to remove selected tasks?") + Row ( + modifier = Modifier.padding(top = 10.dp).fillMaxWidth().clickable { + alsoDeleteFiles = !alsoDeleteFiles + }, + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(checked = alsoDeleteFiles, onCheckedChange = { + alsoDeleteFiles = it + }) + Text("Also delete files") + } + } else { + Text("Are you sure you want to remove all tasks?") + } + } + }, confirmButton = { Button( onClick = { - context.taskManager.clearAllTasks() - recentTasks.clear() - activeTasks.forEach { - runCatching { - it.cancel() - }.onFailure { throwable -> - context.log.error("Failed to cancel task $it", throwable) + showConfirmDialog = false + + if (taskSelection.isNotEmpty()) { + taskSelection.forEach { (task, documentFile) -> + context.taskManager.removeTask(task) + recentTasks.remove(task) + if (alsoDeleteFiles) { + documentFile?.delete() + } + } + activeTasks = activeTasks.filter { task -> !taskSelection.map { it.first }.contains(task.task) } + taskSelection.clear() + } else { + context.taskManager.clearAllTasks() + recentTasks.clear() + activeTasks.forEach { + runCatching { + it.cancel() + }.onFailure { throwable -> + context.log.error("Failed to cancel task $it", throwable) + } } + activeTasks = listOf() + context.taskManager.getActiveTasks().clear() } - activeTasks = listOf() - context.taskManager.getActiveTasks().clear() - showConfirmDialog = false } ) { Text("Yes") @@ -81,6 +255,16 @@ class TasksSection : Section() { var taskStatus by remember { mutableStateOf(task.status) } var taskProgressLabel by remember { mutableStateOf(null) } var taskProgress by remember { mutableIntStateOf(-1) } + val isSelected by remember { derivedStateOf { taskSelection.any { it.first == task } } } + var documentFile by remember { mutableStateOf(null) } + var isDocumentFileReadable by remember { mutableStateOf(true) } + + LaunchedEffect(taskStatus.key) { + launch(Dispatchers.IO) { + documentFile = DocumentFile.fromSingleUri(context.androidContext, task.extra?.toUri() ?: return@launch) + isDocumentFileReadable = documentFile?.canRead() ?: false + } + } val listener = remember { PendingTaskListener( onStateChange = { @@ -102,7 +286,19 @@ class TasksSection : Section() { } } - OutlinedCard(modifier = modifier) { + OutlinedCard(modifier = modifier.clickable { + if (isSelected) { + taskSelection.removeIf { it.first == task } + return@clickable + } + taskSelection.add(task to documentFile) + }.let { + if (isSelected) { + it + .border(2.dp, MaterialTheme.colorScheme.primary) + .clip(MaterialTheme.shapes.medium) + } else it + }) { Row( modifier = Modifier.padding(15.dp), verticalAlignment = Alignment.CenterVertically @@ -110,15 +306,35 @@ class TasksSection : Section() { Column( modifier = Modifier.padding(end = 15.dp) ) { - when (task.type) { - TaskType.DOWNLOAD -> Icon(Icons.Filled.Download, contentDescription = "Download") - TaskType.CHAT_ACTION -> Icon(Icons.Filled.ChatBubble, contentDescription = "Chat Action") + documentFile?.let { file -> + val mimeType = file.type ?: "" + when { + !isDocumentFileReadable -> Icon(Icons.Filled.DeleteOutline, contentDescription = "File not found") + mimeType.contains("image") -> Icon(Icons.Filled.Image, contentDescription = "Image") + mimeType.contains("video") -> Icon(Icons.Filled.Videocam, contentDescription = "Video") + mimeType.contains("audio") -> Icon(Icons.Filled.MusicNote, contentDescription = "Audio") + else -> Icon(Icons.Filled.FileCopy, contentDescription = "File") + } + } ?: run { + when (task.type) { + TaskType.DOWNLOAD -> Icon(Icons.Filled.Download, contentDescription = "Download") + TaskType.CHAT_ACTION -> Icon(Icons.Filled.ChatBubble, contentDescription = "Chat Action") + } } } Column( modifier = Modifier.weight(1f), ) { - Text(task.title, style = MaterialTheme.typography.bodyLarge) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text(task.title, style = MaterialTheme.typography.bodyMedium) + task.author?.takeIf { it != "null" }?.let { + Spacer(modifier = Modifier.width(5.dp)) + Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } Text(task.hash, style = MaterialTheme.typography.labelSmall) Column( modifier = Modifier.padding(top = 5.dp), @@ -183,27 +399,25 @@ class TasksSection : Section() { val tasks = context.taskManager.fetchStoredTasks(lastFetchedTaskId ?: Long.MAX_VALUE) if (tasks.isNotEmpty()) { lastFetchedTaskId = tasks.keys.last() - scope.launch { - val activeTaskIds = activeTasks.map { it.taskId } - recentTasks.addAll(tasks.filter { it.key !in activeTaskIds }.values) - } + val activeTaskIds = activeTasks.map { it.taskId } + recentTasks.addAll(tasks.filter { it.key !in activeTaskIds }.values) } } } - fun fetchActiveTasks() { - activeTasks = context.taskManager.getActiveTasks().values.sortedByDescending { it.taskId }.toMutableList() + LaunchedEffect(Unit) { + fetchActiveTasks(this) } - LaunchedEffect(Unit) { - fetchActiveTasks() + DisposableEffect(Unit) { + onDispose { + taskSelection.clear() + } } OnLifecycleEvent { _, event -> if (event == Lifecycle.Event.ON_RESUME) { - scope.launch { - fetchActiveTasks() - } + fetchActiveTasks(scope) } } @@ -218,12 +432,14 @@ class TasksSection : Section() { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - Icon(Icons.Filled.CheckCircle, contentDescription = "No tasks", tint = MaterialTheme.colorScheme.primary) - Text("No tasks", style = MaterialTheme.typography.bodyLarge) + context.translation["manager.sections.tasks.no_tasks"].let { + Icon(Icons.Filled.CheckCircle, contentDescription = it, tint = MaterialTheme.colorScheme.primary) + Text(it, style = MaterialTheme.typography.bodyLarge) + } } } } - items(activeTasks, key = { it.task.hash }) {pendingTask -> + items(activeTasks, key = { it.taskId }) {pendingTask -> TaskCard(modifier = Modifier.padding(8.dp), pendingTask.task, pendingTask = pendingTask) } items(recentTasks, key = { it.hash }) { task -> @@ -231,7 +447,7 @@ class TasksSection : Section() { } item { Spacer(modifier = Modifier.height(20.dp)) - SideEffect { + LaunchedEffect(remember { derivedStateOf { scrollState.firstVisibleItemIndex } }) { fetchNewRecentTasks() } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/LoggedStories.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/LoggedStories.kt index 72b870f63..ce83fb743 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/LoggedStories.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/LoggedStories.kt @@ -118,7 +118,7 @@ fun LoggedStories( Button(onClick = { val mediaAuthor = friendInfo?.mutableUsername ?: userId - val uniqueHash = selectedStory?.url?.longHashCode()?.absoluteValue?.toString(16) ?: UUID.randomUUID().toString() + val uniqueHash = (selectedStory?.url ?: UUID.randomUUID().toString()).longHashCode().absoluteValue.toString(16) DownloadProcessor( remoteSideContext = context, @@ -150,7 +150,7 @@ fun LoggedStories( ), iconUrl = null, mediaAuthor = friendInfo?.mutableUsername ?: userId, - downloadSource = MediaDownloadSource.STORY_LOGGER.key + downloadSource = MediaDownloadSource.STORY_LOGGER.translate(context.translation), )) }) { Text(text = "Download") diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 7287ae4c8..635e1ae9c 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -38,8 +38,8 @@ "export_logs_button": "Export Logs" } }, - "downloads": { - "empty_download_list": "(empty)" + "tasks": { + "no_tasks": "No tasks" }, "features": { "disabled": "Disabled" @@ -899,6 +899,18 @@ "STATUS_COUNTDOWN": "Countdown" }, + "media_download_source": { + "none": "None", + "pending": "Pending", + "chat_media": "Chat Media", + "story": "Story", + "public_story": "Public Story", + "spotlight": "Spotlight", + "profile_picture": "Profile Picture", + "story_logger": "Story Logger", + "merged": "Merged" + }, + "chat_action_menu": { "preview_button": "Preview", "download_button": "Download", diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/FileType.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/FileType.kt index dcad88b25..6f9672517 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/FileType.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/FileType.kt @@ -13,6 +13,8 @@ enum class FileType( GIF("gif", "image/gif", false, false, false), PNG("png", "image/png", false, true, false), MP4("mp4", "video/mp4", true, false, false), + MKV("mkv", "video/mkv", true, false, false), + AVI("avi", "video/avi", true, false, false), MP3("mp3", "audio/mp3",false, false, true), OPUS("opus", "audio/opus", false, false, true), AAC("aac", "audio/aac", false, false, true), @@ -34,6 +36,9 @@ enum class FileType( "4f676753" to OPUS, "fff15" to AAC, "ffd8ff" to JPG, + "47494638" to GIF, + "1a45dfa3" to MKV, + "52494646" to AVI, ) fun fromString(string: String?): FileType { diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/download/DownloadRequest.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/download/DownloadRequest.kt index 9f40f904d..323d8bdd6 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/download/DownloadRequest.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/download/DownloadRequest.kt @@ -40,12 +40,12 @@ fun createNewFilePath( config: RootConfig, hexHash: String, downloadSource: MediaDownloadSource, - mediaAuthor: String, + mediaAuthor: String?, creationTimestamp: Long? ): String { val pathFormat by config.downloader.pathFormat val customPathFormat by config.downloader.customPathFormat - val sanitizedMediaAuthor = mediaAuthor.sanitizeForPath().ifEmpty { hexHash } + val sanitizedMediaAuthor = mediaAuthor?.sanitizeForPath() ?: hexHash val currentDateTime = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ENGLISH).format(creationTimestamp ?: System.currentTimeMillis()) val finalPath = StringBuilder() diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/download/MediaDownloadSource.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/download/MediaDownloadSource.kt index 034a72a6d..2ceb6bcdc 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/download/MediaDownloadSource.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/download/MediaDownloadSource.kt @@ -1,25 +1,31 @@ package me.rhunk.snapenhance.common.data.download +import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper + enum class MediaDownloadSource( val key: String, - val displayName: String = key, val pathName: String = key, val ignoreFilter: Boolean = false ) { - NONE("none", "None", ignoreFilter = true), - PENDING("pending", "Pending", ignoreFilter = true), - CHAT_MEDIA("chat_media", "Chat Media", "chat_media"), - STORY("story", "Story", "story"), - PUBLIC_STORY("public_story", "Public Story", "public_story"), - SPOTLIGHT("spotlight", "Spotlight", "spotlight"), - PROFILE_PICTURE("profile_picture", "Profile Picture", "profile_picture"), - STORY_LOGGER("story_logger", "Story Logger", "story_logger"); + NONE("none", ignoreFilter = true), + PENDING("pending", ignoreFilter = true), + CHAT_MEDIA("chat_media", "chat_media"), + STORY("story", "story"), + PUBLIC_STORY("public_story", "public_story"), + SPOTLIGHT("spotlight", "spotlight"), + PROFILE_PICTURE("profile_picture", "profile_picture"), + STORY_LOGGER("story_logger", "story_logger"), + MERGED("merged", "merged"); fun matches(source: String?): Boolean { if (source == null) return false return source.contains(key, ignoreCase = true) } + fun translate(translation: LocaleWrapper): String { + return translation["media_download_source.$key"] + } + companion object { fun fromKey(key: String?): MediaDownloadSource { if (key == null) return NONE 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 index fa7b1beaf..ddd5c49bb 100644 --- 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 @@ -102,7 +102,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp metadata = DownloadMetadata( mediaIdentifier = generatedHash, mediaAuthor = mediaAuthor, - downloadSource = downloadSource.key, + downloadSource = downloadSource.translate(context.translation), iconUrl = iconUrl, outputPath = outputPath ), From dd755af5be3be51fab88f09042cfca5d0f76a830 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 31 Dec 2023 00:33:38 +0100 Subject: [PATCH 27/53] feat: export memories --- app/proguard-rules.pro | 1 + common/src/main/assets/lang/en_US.json | 17 + .../snapenhance/common/action/EnumAction.kt | 1 + .../me/rhunk/snapenhance/core/SnapEnhance.kt | 32 +- .../core/action/impl/ExportMemories.kt | 384 ++++++++++++++++++ .../core/manager/impl/ActionManager.kt | 4 +- 6 files changed, 435 insertions(+), 4 deletions(-) create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportMemories.kt diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 7f04e727a..3d8b2a6b2 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -6,5 +6,6 @@ -keep class org.jf.dexlib2.** { *; } -keep class org.mozilla.javascript.** { *; } -keep class androidx.compose.material.icons.** { *; } +-keep class androidx.compose.material3.R$* { *; } -keep class androidx.navigation.** { *; } -keep class me.rhunk.snapenhance.** { *; } diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 635e1ae9c..8954a2697 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -128,6 +128,7 @@ "open_map": "Choose location on map", "check_for_updates": "Check for updates", "export_chat_messages": "Export Chat Messages", + "export_memories": "Export Memories", "bulk_messaging_action": "Bulk Messaging Action" }, @@ -1075,5 +1076,21 @@ "suspend_location_updates": { "switch_text": "Suspend Location Updates" + }, + "material3_strings": { + "date_range_input_title": "", + "date_range_picker_start_headline": "From", + "date_range_picker_end_headline": "To", + "date_range_picker_title": "Select date range", + "date_picker_switch_to_calendar_mode": "Calendar", + "date_picker_switch_to_input_mode": "Input", + "date_range_picker_scroll_to_previous_month": "Previous month", + "date_range_picker_scroll_to_next_month": "Next month", + "date_picker_today_description": "Today", + "date_range_picker_day_in_range": "Selected", + "date_input_invalid_for_pattern": "Invalid date", + "date_input_invalid_year_range": "Invalid year", + "date_input_invalid_not_allowed": "Invalid date", + "date_range_input_invalid_range_input": "Invalid date range" } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/action/EnumAction.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/action/EnumAction.kt index 415118a48..b7aa535b3 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/action/EnumAction.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/action/EnumAction.kt @@ -9,6 +9,7 @@ enum class EnumAction( ) { CLEAN_CACHE("clean_snapchat_cache", exitOnFinish = true), EXPORT_CHAT_MESSAGES("export_chat_messages"), + EXPORT_MEMORIES("export_memories"), BULK_MESSAGING_ACTION("bulk_messaging_action"); companion object { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt index a2199184f..1ed311970 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -3,6 +3,7 @@ package me.rhunk.snapenhance.core import android.app.Activity import android.app.Application import android.content.Context +import android.content.res.Resources import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -20,8 +21,10 @@ import me.rhunk.snapenhance.core.data.SnapClassCache import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent import me.rhunk.snapenhance.core.util.LSPatchUpdater +import me.rhunk.snapenhance.core.util.hook.HookAdapter import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook +import java.lang.reflect.Modifier import kotlin.system.measureTimeMillis @@ -36,11 +39,11 @@ class SnapEnhance { private lateinit var appContext: ModContext private var isBridgeInitialized = false - private fun hookMainActivity(methodName: String, stage: HookStage = HookStage.AFTER, block: Activity.() -> Unit) { + private fun hookMainActivity(methodName: String, stage: HookStage = HookStage.AFTER, block: Activity.(param: HookAdapter) -> Unit) { Activity::class.java.hook(methodName, stage, { isBridgeInitialized }) { param -> val activity = param.thisObject() as Activity if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return@hook - block(activity) + block(activity, param) } } @@ -90,6 +93,8 @@ class SnapEnhance { appContext.mainActivity = this if (isMainActivityNotNull || !appContext.mappings.isMappingsLoaded()) return@hookMainActivity onActivityCreate() + jetpackComposeResourceHook() + appContext.actionManager.onNewIntent(intent) } hookMainActivity("onPause") { @@ -97,6 +102,10 @@ class SnapEnhance { appContext.isMainActivityPaused = true } + hookMainActivity("onNewIntent") { param -> + 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 @@ -107,7 +116,6 @@ class SnapEnhance { return@hookMainActivity } - appContext.actionManager.onNewIntent(this.intent) appContext.reloadConfig() syncRemote() } @@ -263,4 +271,22 @@ class SnapEnhance { } } } + + private fun jetpackComposeResourceHook() { + val material3RString = try { + Class.forName("androidx.compose.material3.R\$string") + } catch (e: ClassNotFoundException) { + return + } + + val stringResources = material3RString.fields.filter { + Modifier.isStatic(it.modifiers) && it.type == Int::class.javaPrimitiveType + }.associate { it.getInt(null) to it.name } + + Resources::class.java.getMethod("getString", Int::class.javaPrimitiveType).hook(HookStage.BEFORE) { param -> + val key = param.arg(0) + val name = stringResources[key] ?: return@hook + param.setResult(appContext.translation.getOrNull("material3_strings.$name") ?: return@hook) + } + } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportMemories.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportMemories.kt new file mode 100644 index 000000000..adfb586b6 --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportMemories.kt @@ -0,0 +1,384 @@ +package me.rhunk.snapenhance.core.action.impl + +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteDatabase.OpenParams +import android.os.Environment +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.* +import me.rhunk.snapenhance.common.data.FileType +import me.rhunk.snapenhance.common.ui.createComposeAlertDialog +import me.rhunk.snapenhance.common.util.ktx.getLongOrNull +import me.rhunk.snapenhance.common.util.ktx.getStringOrNull +import me.rhunk.snapenhance.core.action.AbstractAction +import okhttp3.OkHttpClient +import java.io.File +import java.io.FileOutputStream +import java.nio.file.attribute.FileTime +import java.time.Instant +import java.time.OffsetDateTime +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.math.absoluteValue + +class ExportMemories : AbstractAction() { + data class TimeRange( + val start: Long?, + val end: Long?, + ) + + data class MemoriesEntry( + val storyTitle: String, + val createTime: Long, + val mediaKey: String?, + val mediaIv: String?, + val downloadUrl: String + ) { + val folderName: String + get() = storyTitle.replace(Regex("[^a-zA-Z0-9\\s]"), "").trim().replace(Regex("\\s+"), "_") + } + + @OptIn(ExperimentalCoroutinesApi::class, ExperimentalEncodingApi::class) + private suspend fun exportMemories( + scope: CoroutineScope = context.coroutineScope, + database: SQLiteDatabase, + timeRange: TimeRange?, + includeMEO: Boolean, + folders: Boolean, + progress: (Int, Int) -> Unit + ) { + val downloadContext = Dispatchers.IO.limitedParallelism(10) + val writeToZipContext = Dispatchers.IO.limitedParallelism(1) + val outputZip = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "memories_" + System.currentTimeMillis() + ".zip").also { + if (it.exists()) it.delete() + } + val okHttpClient = OkHttpClient.Builder().build() + val outputZipFile = withContext(Dispatchers.IO) { + ZipOutputStream(FileOutputStream(outputZip)).apply { + setComment("Exported from SnapEnhance") + setMethod(ZipOutputStream.DEFLATED) + } + } + var totalCount = 0 + var currentCount = 0 + var failed = 0 + + fun updateProgress() { + progress((currentCount.toFloat() / totalCount.toFloat() * 100f).toInt(), failed) + } + + val jobs = mutableListOf() + + val meoMasterKeyPair = if (includeMEO) { + runCatching { + database.rawQuery("SELECT * FROM memories_meo_confidential", null).use { cursor -> + if (cursor.moveToNext()) { + cursor.getStringOrNull("master_key")!!.trim() to cursor.getStringOrNull("master_key_iv")!!.trim() + } else null + } + }.getOrNull() + } else null + + database.rawQuery("SELECT memories_entry.title as story_title, memories_snap.create_time, " + + "memories_snap.media_key, memories_snap.media_iv, memories_snap.encrypted_media_key, memories_snap.encrypted_media_iv, " + + "memories_media.download_url FROM memories_snap " + + "INNER JOIN memories_entry ON memories_snap.memories_entry_id = memories_entry._id " + + "INNER JOIN memories_media ON memories_snap.media_id = memories_media._id " + + "WHERE memories_snap.create_time >= ? AND memories_snap.create_time <= ? " + + "ORDER BY memories_snap.create_time ASC", arrayOf(timeRange?.start?.toString() ?: "-1", timeRange?.end?.toString() ?: Long.MAX_VALUE.toString()) + ).use { cursor -> + while (cursor.moveToNext()) { + val encryptedMediaKey = cursor.getStringOrNull("encrypted_media_key")?.trim() + val encryptedMediaIv = cursor.getStringOrNull("encrypted_media_iv")?.trim() + var mediaKey = cursor.getStringOrNull("media_key")?.trim() + var mediaIv = cursor.getStringOrNull("media_iv")?.trim() + + if (!includeMEO && encryptedMediaKey != null && encryptedMediaIv != null) continue + + meoMasterKeyPair.takeIf { encryptedMediaKey != null && encryptedMediaIv != null }?.let { keyPair -> + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + runCatching { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(Base64.decode(keyPair.first), "AES"), IvParameterSpec(Base64.decode(keyPair.second))) + mediaKey = Base64.encode(cipher.doFinal(Base64.decode(encryptedMediaKey ?: return@let))) + mediaIv = Base64.encode(cipher.doFinal(Base64.decode(encryptedMediaIv ?: return@let))) + context.log.verbose("decrypted meo $mediaKey/$mediaIv") + }.onFailure { + context.log.error("failed to decrypt meo", it) + } + } + + if (mediaKey == null || mediaIv == null) { + context.log.error("missing media key or iv for ${cursor.getStringOrNull("download_url")}") + failed++ + updateProgress() + continue + } + + val entry = MemoriesEntry( + storyTitle = cursor.getStringOrNull("story_title") ?: "unknown", + createTime = cursor.getLongOrNull("create_time") ?: -1L, + mediaKey = mediaKey, + mediaIv = mediaIv, + downloadUrl = cursor.getStringOrNull("download_url") ?: continue + ) + + totalCount++ + + scope.launch(downloadContext) { + var downloadedFile = File.createTempFile("memories", ".tmp", context.androidContext.cacheDir) + + runCatching { + okHttpClient.newCall( + okhttp3.Request.Builder() + .url(entry.downloadUrl) + .build() + ).execute().use { response -> + val inputStream = response.body.byteStream().let { + if (entry.mediaKey != null && entry.mediaIv != null) { + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(Base64.decode(entry.mediaKey), "AES"), IvParameterSpec(Base64.decode(entry.mediaIv))) + CipherInputStream(it, cipher) + } else it + } + + downloadedFile.outputStream().use { outputStream -> + inputStream.use { inputStream -> + inputStream.copyTo(outputStream) + } + } + + val fileType = FileType.fromFile(downloadedFile) + + downloadedFile = File( + downloadedFile.parentFile, + "${entry.createTime}-${entry.downloadUrl.hashCode().absoluteValue.toString(16)}.${fileType.fileExtension}" + ).also { + downloadedFile.renameTo(it) + } + + withContext(writeToZipContext) { + val zipEntry = ZipEntry("${if (folders) entry.folderName + "/" else entry.folderName}${downloadedFile.name}") + FileTime.fromMillis(entry.createTime).let { + zipEntry.lastModifiedTime = it + zipEntry.lastAccessTime = it + zipEntry.creationTime = it + } + outputZipFile.apply { + putNextEntry(zipEntry) + downloadedFile.inputStream().use { it.copyTo(outputZipFile) } + closeEntry() + flush() + } + currentCount++ + updateProgress() + } + } + }.onFailure { + context.log.error("failed to download ${entry.downloadUrl}", it) + failed++ + updateProgress() + } + downloadedFile.delete() + }.also { jobs.add(it) } + } + } + + jobs.joinAll() + withContext(Dispatchers.IO) { + outputZipFile.close() + } + context.longToast("Exported to ${outputZip.absolutePath}") + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun ExporterDialog(database: SQLiteDatabase, onDismiss: () -> Unit) { + var exportJob by remember { mutableStateOf(null as Job?) } + var exportFinished by remember { mutableStateOf(false) } + var exportProgress by remember { mutableStateOf(Pair(0, 0)) } // progress, failed + + var dateRangeFilter by remember { mutableStateOf(false) } + var sortByFolder by remember { mutableStateOf(false) } + var includeMEO by remember { mutableStateOf(false) } + val dateRangePickerState = rememberDateRangePickerState( + initialSelectedStartDateMillis = OffsetDateTime.now().minusDays(8).toInstant().toEpochMilli(), + initialSelectedEndDateMillis = Instant.now().toEpochMilli(), + initialDisplayMode = DisplayMode.Input + ) + + val totalCount = remember(dateRangePickerState.selectedStartDateMillis, dateRangePickerState.selectedEndDateMillis, dateRangeFilter) { + val timeRange = dateRangePickerState.takeIf { dateRangeFilter }?.let { + TimeRange(it.selectedStartDateMillis, it.selectedEndDateMillis) + } + + database.rawQuery("SELECT COUNT(*) FROM memories_snap WHERE create_time >= ? AND create_time <= ? ", arrayOf(timeRange?.start?.toString() ?: "-1", timeRange?.end?.toString() ?: Long.MAX_VALUE.toString())).use { + it.moveToFirst() + it.getInt(0) + } + } + + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("Export memories", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, fontSize = 20.sp) + + if (exportJob != null) { + Text(text = "Exporting memories... (${exportProgress.second} failed)", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center) + LinearProgressIndicator(progress = exportProgress.first / 100f, Modifier.fillMaxWidth()) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + Button(onClick = { + exportJob?.cancel() + exportJob = null + onDismiss() + }) { + Text("Quit") + } + if (exportFinished) { + Button(onClick = { + exportJob = null + onDismiss() + }) { + Text("Done") + } + } + } + } else { + Text("Total memories: $totalCount", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + var dateRangeDialog by remember { mutableStateOf(false) } + Checkbox(checked = dateRangeFilter, onCheckedChange = { dateRangeFilter = it }) + Text("Date Range", modifier = Modifier.weight(1f)) + Button(onClick = { dateRangeDialog = true }, enabled = dateRangeFilter) { + Text("Select") + } + + if (dateRangeDialog) { + DatePickerDialog(onDismissRequest = { + dateRangeDialog = false + }, confirmButton = {}) { + DateRangePicker( + state = dateRangePickerState, + modifier = Modifier.weight(1f), + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.End + ) { + Button(onClick = { + dateRangeDialog = false + }) { + Text("OK") + } + } + } + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(checked = sortByFolder, onCheckedChange = { sortByFolder = it }) + Text("Sort by folder", modifier = Modifier.weight(1f)) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(checked = includeMEO, onCheckedChange = { includeMEO = it }) + Text("Include My Eyes Only", modifier = Modifier.weight(1f)) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + Button(onClick = onDismiss) { + Text("Cancel") + } + Button(onClick = { + context.coroutineScope.launch { + exportMemories( + scope = this, + database = database, + timeRange = dateRangePickerState.takeIf { dateRangeFilter }?.let { + TimeRange(it.selectedStartDateMillis, it.selectedEndDateMillis) + }, + folders = sortByFolder, + includeMEO = includeMEO, + ) { progress, failed -> + exportProgress = Pair(progress, failed) + } + }.also { exportJob = it }.invokeOnCompletion { + exportFinished = true + } + }) { + Text("Export") + } + } + } + + + } + } + + override fun run() { + context.coroutineScope.launch(Dispatchers.Main) { + val database = runCatching { + SQLiteDatabase.openDatabase( + context.androidContext.getDatabasePath("memories.db"), + OpenParams.Builder().build(), + ) + }.getOrNull() + + if (database == null) { + context.longToast("Failed to open memories database") + return@launch + } + + createComposeAlertDialog(context.mainActivity!!) { alertDialog -> + ExporterDialog(database) { alertDialog.dismiss() } + }.apply { + setOnDismissListener { database.close() } + setCanceledOnTouchOutside(false) + show() + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/ActionManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/ActionManager.kt index 56af46a91..d137e07bc 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/ActionManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/ActionManager.kt @@ -6,6 +6,7 @@ import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.action.impl.BulkMessagingAction import me.rhunk.snapenhance.core.action.impl.CleanCache import me.rhunk.snapenhance.core.action.impl.ExportChatMessages +import me.rhunk.snapenhance.core.action.impl.ExportMemories import me.rhunk.snapenhance.core.manager.Manager class ActionManager( @@ -17,6 +18,7 @@ class ActionManager( EnumAction.CLEAN_CACHE to CleanCache::class, EnumAction.EXPORT_CHAT_MESSAGES to ExportChatMessages::class, EnumAction.BULK_MESSAGING_ACTION to BulkMessagingAction::class, + EnumAction.EXPORT_MEMORIES to ExportMemories::class, ).map { it.key to it.value.java.getConstructor().newInstance().apply { this.context = modContext @@ -29,8 +31,8 @@ class ActionManager( fun onNewIntent(intent: Intent?) { val action = intent?.getStringExtra(EnumAction.ACTION_PARAMETER) ?: return - execute(EnumAction.entries.find { it.key == action } ?: return) intent.removeExtra(EnumAction.ACTION_PARAMETER) + execute(EnumAction.entries.find { it.key == action } ?: return) } fun execute(enumAction: EnumAction) { From 1b566db184f19d349e2c28a0ee1cb8f939ee959a Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 31 Dec 2023 15:56:41 +0100 Subject: [PATCH 28/53] refactor: ProfilePictureDownloader --- .../impl/downloader/ProfilePictureDownloader.kt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/ProfilePictureDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/ProfilePictureDownloader.kt index 1a60ee957..b0c8014dc 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/ProfilePictureDownloader.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/ProfilePictureDownloader.kt @@ -59,15 +59,8 @@ class ProfilePictureDownloader : Feature("ProfilePictureDownloader", loadParams context.event.subscribe(NetworkApiRequestEvent::class) { event -> if (!event.url.endsWith("/rpc/getPublicProfile")) return@subscribe - Hooker.ephemeralHookObjectMethod(event.callback::class.java, event.callback, "onSucceeded", HookStage.BEFORE) { methodParams -> - val content = methodParams.arg(2).run { - ByteArray(capacity()).also { - get(it) - position(0) - } - } - - ProtoReader(content).followPath(1, 1, 2) { + event.onSuccess { buffer -> + ProtoReader(buffer ?: return@onSuccess).followPath(1, 1, 2) { friendUsername = getString(2) ?: return@followPath followPath(4) { backgroundUrl = getString(2) From f1b0bc41f2410c2c72dd9c9a14f4d3b90fcf3e1b Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:41:12 +0100 Subject: [PATCH 29/53] feat: loop media playback - refactor mappings wrapper --- common/src/main/assets/lang/en_US.json | 4 ++ .../common/bridge/wrapper/MappingsWrapper.kt | 24 +++---- .../common/config/impl/MessagingTweaks.kt | 1 + .../core/action/impl/BulkMessagingAction.kt | 2 +- .../features/impl/ConfigurationOverride.kt | 2 +- .../impl/OperaViewerParamsOverride.kt | 64 +++++++++++++++++++ .../impl/experiments/MeoPasscodeBypass.kt | 2 +- .../global/BypassVideoLengthRestriction.kt | 4 +- .../impl/global/MediaQualityLevelOverride.kt | 4 +- .../impl/ui/HideQuickAddFriendFeed.kt | 2 +- .../core/manager/impl/FeatureManager.kt | 2 + .../mapper/impl/OperaViewerParamsMapper.kt | 32 ++++++++++ .../snapenhance/mapper/tests/TestMappings.kt | 1 + 13 files changed, 121 insertions(+), 23 deletions(-) create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 8954a2697..80ab676fd 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -378,6 +378,10 @@ "name": "Unlimited Snap View Time", "description": "Removes the Time Limit for viewing Snaps" }, + "loop_media_playback": { + "name": "Loop Media Playback", + "description": "Loops media playback when viewing Snaps / Stories" + }, "disable_replay_in_ff": { "name": "Disable Replay in FF", "description": "Disables the ability to replay with a long press from the Friend Feed" diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt index 337b3a25a..3f341925b 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt @@ -31,6 +31,7 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr FriendRelationshipChangerMapper::class, ViewBinderMapper::class, FriendingDataSourcesMapper::class, + OperaViewerParamsMapper::class, ) } @@ -115,29 +116,22 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr return mappings[key] } - fun getMappedClass(className: String): Class<*> { - return context.classLoader.loadClass(getMappedObject(className) as String) + fun getMappedClass(className: String): Class<*>? { + return runCatching { + context.classLoader.loadClass(getMappedObject(className) as? String) + }.getOrNull() } fun getMappedClass(key: String, subKey: String): Class<*> { return context.classLoader.loadClass(getMappedValue(key, subKey)) } - fun getMappedValue(key: String): String { - return getMappedObject(key) as String + fun getMappedValue(key: String, subKey: String): String? { + return getMappedMap(key)?.get(subKey) as? String } @Suppress("UNCHECKED_CAST") - fun getMappedList(key: String): List { - return listOf(getMappedObject(key) as List).flatten() - } - - fun getMappedValue(key: String, subKey: String): String { - return getMappedMap(key)[subKey] as String - } - - @Suppress("UNCHECKED_CAST") - fun getMappedMap(key: String): Map { - return getMappedObject(key) as Map + fun getMappedMap(key: String): Map? { + return getMappedObjectNullable(key) as? Map } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt index cc60d46b8..0ccd40513 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt @@ -55,6 +55,7 @@ class MessagingTweaks : ConfigContainer() { val hideBitmojiPresence = boolean("hide_bitmoji_presence") val hideTypingNotifications = boolean("hide_typing_notifications") val unlimitedSnapViewTime = boolean("unlimited_snap_view_time") + val loopMediaPlayback = boolean("loop_media_playback") { requireRestart() } val disableReplayInFF = boolean("disable_replay_in_ff") val halfSwipeNotifier = container("half_swipe_notifier", HalfSwipeNotifierConfig()) { requireRestart()} val messagePreviewLength = integer("message_preview_length", defaultValue = 20) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt index b4af2f4c7..3ded9b814 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt @@ -145,7 +145,7 @@ class BulkMessagingAction : AbstractAction() { } private fun removeFriend(userId: String) { - val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger") + val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger") ?: throw Exception("Failed to get FriendRelationshipChanger mapping") val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance!! val removeFriendMethod = friendRelationshipChangerInstance::class.java.methods.first { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt index 457214a2f..1b14deec9 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt @@ -23,7 +23,7 @@ data class ConfigFilter( class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { - val compositeConfigurationProviderMappings = context.mappings.getMappedMap("CompositeConfigurationProvider") + val compositeConfigurationProviderMappings = context.mappings.getMappedMap("CompositeConfigurationProvider") ?: throw Exception("Failed to get compositeConfigurationProviderMappings") val enumMappings = compositeConfigurationProviderMappings["enum"] as Map<*, *> fun getConfigKeyInfo(key: Any?) = runCatching { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt new file mode 100644 index 000000000..e6551a65f --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt @@ -0,0 +1,64 @@ +package me.rhunk.snapenhance.core.features.impl + +import me.rhunk.snapenhance.core.features.Feature +import me.rhunk.snapenhance.core.features.FeatureLoadParams +import me.rhunk.snapenhance.core.util.hook.HookStage +import me.rhunk.snapenhance.core.util.hook.hook + +class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + data class OverrideKey( + val name: String, + val defaultValue: Any? + ) + + data class Override( + val filter: (value: Any?) -> Boolean, + val value: (key: OverrideKey, value: Any?) -> Any? + ) + + override fun onActivityCreate() { + val operaViewerParamsMappings = context.mappings.getMappedMap("OperaViewerParams") ?: throw Exception("Failed to get operaViewerParamsMappings") + val overrideMap = mutableMapOf() + + fun overrideParam(key: String, filter: (value: Any?) -> Boolean, value: (overrideKey: OverrideKey, value: Any?) -> Any?) { + overrideMap[key] = Override(filter, value) + } + + if (context.config.messaging.loopMediaPlayback.get()) { + //https://github.com/rodit/SnapMod/blob/master/app/src/main/java/xyz/rodit/snapmod/features/opera/SnapDurationModifier.kt + overrideParam("auto_advance_mode", { true }, { key, _ -> key.defaultValue }) + overrideParam("auto_advance_max_loop_number", { true }, { _, _ -> Int.MAX_VALUE }) + overrideParam("media_playback_mode", { true }, { _, value -> + val playbackMode = value ?: return@overrideParam null + playbackMode::class.java.enumConstants.firstOrNull { + it.toString() == "LOOPING" + } ?: return@overrideParam value + }) + } + + findClass(operaViewerParamsMappings["class"].toString()).hook(operaViewerParamsMappings["putMethod"].toString(), HookStage.BEFORE) { param -> + val key = param.argNullable(0)?.let { key -> + val fields = key::class.java.fields + OverrideKey( + name = fields.firstOrNull { + it.type == String::class.java + }?.get(key)?.toString() ?: return@hook, + defaultValue = fields.firstOrNull { + it.type == Object::class.java + }?.get(key) + ) + } ?: return@hook + val value = param.argNullable(1) ?: return@hook + + overrideMap[key.name]?.let { override -> + if (override.filter(value)) { + runCatching { + param.setArg(1, override.value(key, value)) + }.onFailure { + context.log.error("Failed to override param $key", it) + } + } + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt index 400ada485..1b6157ae2 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt @@ -7,7 +7,7 @@ import me.rhunk.snapenhance.core.util.hook.Hooker class MeoPasscodeBypass : Feature("Meo Passcode Bypass", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { - val bcrypt = context.mappings.getMappedMap("BCrypt") + val bcrypt = context.mappings.getMappedMap("BCrypt") ?: throw Exception("Failed to get bcrypt mappings") Hooker.hook( context.androidContext.classLoader.loadClass(bcrypt["class"].toString()), diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt index a34c2d5c4..cbd3dae4b 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt @@ -46,7 +46,7 @@ class BypassVideoLengthRestriction : } context.mappings.getMappedClass("DefaultMediaItem") - .hookConstructor(HookStage.BEFORE) { param -> + ?.hookConstructor(HookStage.BEFORE) { param -> //set the video length argument param.setArg(5, -1L) } @@ -54,7 +54,7 @@ class BypassVideoLengthRestriction : //TODO: allow split from any source if (mode == "split") { - val cameraRollId = context.mappings.getMappedMap("CameraRollMediaId") + val cameraRollId = context.mappings.getMappedMap("CameraRollMediaId") ?: throw Exception("Failed to get cameraRollId mappings") // memories grid findClass(cameraRollId["class"].toString()).hookConstructor(HookStage.AFTER) { param -> //set the durationMs field diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaQualityLevelOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaQualityLevelOverride.kt index d213fa67d..0c8607200 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaQualityLevelOverride.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaQualityLevelOverride.kt @@ -7,8 +7,8 @@ import me.rhunk.snapenhance.core.util.hook.hook class MediaQualityLevelOverride : Feature("MediaQualityLevelOverride", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { - val enumQualityLevel = context.mappings.getMappedClass("EnumQualityLevel") - val mediaQualityLevelProvider = context.mappings.getMappedMap("MediaQualityLevelProvider") + val enumQualityLevel = context.mappings.getMappedClass("EnumQualityLevel") ?: throw Exception("Failed to get enumQualityLevelMappings") + val mediaQualityLevelProvider = context.mappings.getMappedMap("MediaQualityLevelProvider") ?: throw Exception("Failed to get mediaQualityLevelProviderMappings") val forceMediaSourceQuality by context.config.global.forceUploadSourceQuality diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt index 68efa4a02..2f2b73042 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt @@ -10,7 +10,7 @@ class HideQuickAddFriendFeed : Feature("HideQuickAddFriendFeed", loadParams = Fe override fun onActivityCreate() { if (!context.config.userInterface.hideQuickAddFriendFeed.get()) return - val friendingDataSource = context.mappings.getMappedMap("FriendingDataSources") + val friendingDataSource = context.mappings.getMappedMap("FriendingDataSources") ?: throw Exception("Failed to get friendingDataSourceMappings") findClass(friendingDataSource["class"].toString()).hookConstructor(HookStage.AFTER) { param -> param.thisObject().setObjectField( friendingDataSource["quickAddSourceListField"].toString(), diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt index 6e0df5101..dded65f09 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt @@ -8,6 +8,7 @@ import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.impl.ConfigurationOverride +import me.rhunk.snapenhance.core.features.impl.OperaViewerParamsOverride import me.rhunk.snapenhance.core.features.impl.ScopeSync import me.rhunk.snapenhance.core.features.impl.Stories import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader @@ -120,6 +121,7 @@ class FeatureManager( SuspendLocationUpdates::class, ConversationToolbox::class, SpotlightCommentsUsername::class, + OperaViewerParamsOverride::class, ) initializeFeatures() diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt new file mode 100644 index 000000000..d6de7b768 --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt @@ -0,0 +1,32 @@ +package me.rhunk.snapenhance.mapper.impl + +import me.rhunk.snapenhance.mapper.AbstractClassMapper +import me.rhunk.snapenhance.mapper.ext.findConstString +import me.rhunk.snapenhance.mapper.ext.getClassName +import org.jf.dexlib2.iface.instruction.formats.Instruction35c +import org.jf.dexlib2.iface.reference.MethodReference + +class OperaViewerParamsMapper : AbstractClassMapper() { + init { + mapper { + for (classDef in classes) { + classDef.fields.firstOrNull { it.type == "Ljava/util/concurrent/ConcurrentHashMap;" } ?: continue + if (classDef.methods.firstOrNull { it.name == "toString" }?.implementation?.findConstString("Params") != true) continue + + val putMethod = classDef.methods.firstOrNull { method -> + method.implementation?.instructions?.any { + val instruction = it as? Instruction35c ?: return@any false + val reference = instruction.reference as? MethodReference ?: return@any false + reference.name == "put" && reference.definingClass == "Ljava/util/concurrent/ConcurrentHashMap;" + } == true + } ?: return@mapper + + addMapping("OperaViewerParams", + "class" to classDef.getClassName(), + "putMethod" to putMethod.name + ) + return@mapper + } + } + } +} \ No newline at end of file diff --git a/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt b/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt index 42fa32d4e..9a0eaf5a4 100644 --- a/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt +++ b/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt @@ -25,6 +25,7 @@ class TestMappings { FriendRelationshipChangerMapper::class, ViewBinderMapper::class, FriendingDataSourcesMapper::class, + OperaViewerParamsMapper::class, ) val gson = GsonBuilder().setPrettyPrinting().create() From ebee8f2ef85d238355483917766ccbf496a11e14 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Mon, 1 Jan 2024 23:17:38 +0100 Subject: [PATCH 30/53] feat(core/export_template): sort by date --- core/src/main/assets/web/export_template.html | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/core/src/main/assets/web/export_template.html b/core/src/main/assets/web/export_template.html index adf196818..f9927a935 100644 --- a/core/src/main/assets/web/export_template.html +++ b/core/src/main/assets/web/export_template.html @@ -207,6 +207,11 @@
+
+ +
@@ -247,8 +252,15 @@ } function makeMain() { + document.querySelector('main').innerHTML = "" const messageTemplate = document.querySelector("#message_template") - Object.values(conversationData.messages).forEach(message => { + let messageList = Object.values(conversationData.messages) + + if (document.querySelector(".sort_by_date").checked) { + messageList = messageList.reverse() + } + + messageList.forEach(message => { const messageObject = document.createElement("div") messageObject.classList.add("message") @@ -276,6 +288,7 @@ messageContainer.classList.add("content") messageContainer.innerHTML = message.serializedContent + const observers = [] if (!message.serializedContent) { messageContainer.innerHTML = "" let messageData = "" @@ -288,19 +301,22 @@ messageData = message.type } messageContainer.innerHTML += messageData + messageContainer.onclick = () => { + observers.forEach(f => f()) + } } if (message.attachments && message.attachments.length > 0) { - let observers = [] - message.attachments.forEach((attachment, index) => { const mediaKey = attachment.key.replace(/(=)/g, "") observers.push(() => { + messageContainer.onclick = () => {} const originalMedia = document.querySelector('.media-ORIGINAL_' + mediaKey) if (!originalMedia) { return } + messageContainer.innerHTML = "" const originalMediaUrl = decodeMedia(originalMedia) @@ -340,7 +356,6 @@ new IntersectionObserver(entries => { if (!fetched && entries[0].isIntersecting === true) { fetched = true - messageContainer.innerHTML = "" observers.forEach(c => { try { c() From fcb84661a6fe8af93f55deceffc3ab3d459873fb Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Mon, 1 Jan 2024 23:33:37 +0100 Subject: [PATCH 31/53] fix(mappings): build error --- .../core/features/impl/downloader/MediaDownloader.kt | 11 +++++------ .../features/impl/experiments/AddFriendSourceSpoof.kt | 2 +- .../features/impl/experiments/InfiniteStoryBoost.kt | 7 +++---- .../features/impl/experiments/NoFriendScoreDelay.kt | 2 +- .../core/features/impl/global/SnapchatPlus.kt | 2 +- .../core/features/impl/tweaks/CameraTweaks.kt | 2 +- 6 files changed, 12 insertions(+), 14 deletions(-) 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 index ddd5c49bb..b398669b9 100644 --- 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 @@ -37,7 +37,7 @@ import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.util.hook.HookAdapter import me.rhunk.snapenhance.core.util.hook.HookStage -import me.rhunk.snapenhance.core.util.hook.Hooker +import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.media.PreviewUtils import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID @@ -466,11 +466,11 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param -> - val viewState = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "viewStateField")).toString() + val viewState = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "viewStateField")!!).toString() if (viewState != "FULLY_DISPLAYED") { return@onOperaViewStateCallback } - val operaLayerList = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "layerListField")) as ArrayList<*> + val operaLayerList = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "layerListField")!!) as ArrayList<*> val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list")) @@ -503,9 +503,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } arrayOf("onDisplayStateChange", "onDisplayStateChangeGesture").forEach { methodName -> - Hooker.hook( - operaViewerControllerClass, - context.mappings.getMappedValue("OperaPageViewController", methodName), + operaViewerControllerClass.hook( + context.mappings.getMappedValue("OperaPageViewController", methodName) ?: return@forEach, HookStage.AFTER, onOperaViewStateCallback ) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt index 76fa8eebb..83b5e4475 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt @@ -11,7 +11,7 @@ class AddFriendSourceSpoof : Feature("AddFriendSourceSpoof", loadParams = Featur private set override fun onActivityCreate() { - val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger") + val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger") ?: throw Exception("Failed to get friendRelationshipChangerMapping") findClass(friendRelationshipChangerMapping["class"].toString()).hookConstructor(HookStage.AFTER) { param -> friendRelationshipChangerInstance = param.thisObject() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt index edc27c54e..60b7455b5 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt @@ -7,11 +7,10 @@ import me.rhunk.snapenhance.core.util.hook.hookConstructor class InfiniteStoryBoost : Feature("InfiniteStoryBoost", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { - val storyBoostStateClass = context.mappings.getMappedClass("StoryBoostStateClass") + if (!context.config.experimental.infiniteStoryBoost.get()) return + val storyBoostStateClass = context.mappings.getMappedClass("StoryBoostStateClass") ?: throw Exception("Failed to get storyBoostStateClass") - storyBoostStateClass.hookConstructor(HookStage.BEFORE, { - context.config.experimental.infiniteStoryBoost.get() - }) { param -> + storyBoostStateClass.hookConstructor(HookStage.BEFORE) { param -> val startTimeMillis = param.arg(1) //reset timestamp if it's more than 24 hours if (System.currentTimeMillis() - startTimeMillis > 86400000) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt index 67b1d9d83..c69d3a8c9 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt @@ -9,7 +9,7 @@ import java.lang.reflect.Constructor class NoFriendScoreDelay : Feature("NoFriendScoreDelay", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { override fun onActivityCreate() { if (!context.config.experimental.noFriendScoreDelay.get()) return - val scoreUpdateClass = context.mappings.getMappedClass("ScoreUpdate") + val scoreUpdateClass = context.mappings.getMappedClass("ScoreUpdate") ?: throw Exception("Failed to get scoreUpdateClass") scoreUpdateClass.hookConstructor(HookStage.BEFORE) { param -> val constructor = param.method() as Constructor<*> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt index f6a3e80f0..e553989a8 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt @@ -13,7 +13,7 @@ class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.INIT_ override fun init() { if (!context.config.global.snapchatPlus.get()) return - val subscriptionInfoClass = context.mappings.getMappedClass("SubscriptionInfoClass") + val subscriptionInfoClass = context.mappings.getMappedClass("SubscriptionInfoClass") ?: throw Exception("Failed to get subscriptionInfoClass") Hooker.hookConstructor(subscriptionInfoClass, HookStage.BEFORE) { param -> if (param.arg(0) == 2) return@hookConstructor diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt index 144231570..9ec25a9b5 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt @@ -62,7 +62,7 @@ class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACT } } - context.mappings.getMappedClass("ScCameraSettings").hookConstructor(HookStage.BEFORE) { param -> + context.mappings.getMappedClass("ScCameraSettings")?.hookConstructor(HookStage.BEFORE) { param -> val previewResolution = ScSize(param.argNullable(2)) val captureResolution = ScSize(param.argNullable(3)) From 0b0220ce848afee9a65dc225d8b47909816031ef Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Wed, 3 Jan 2024 01:33:49 +0100 Subject: [PATCH 32/53] feat(app/ui): home section redesign --- .../snapenhance/ui/manager/Navigation.kt | 2 +- .../rhunk/snapenhance/ui/manager/Section.kt | 4 + .../ui/manager/sections/home/HomeSection.kt | 200 +++++++----------- .../manager/sections/home/HomeSubSection.kt | 58 ++++- .../manager/sections/home/SettingsSection.kt | 10 +- app/src/main/res/drawable/ic_github.xml | 11 + app/src/main/res/drawable/ic_telegram.xml | 4 + .../snapenhance/common/action/EnumAction.kt | 4 +- 8 files changed, 164 insertions(+), 129 deletions(-) create mode 100644 app/src/main/res/drawable/ic_github.xml create mode 100644 app/src/main/res/drawable/ic_telegram.xml diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt index f37d9500a..4aaecdaaf 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt @@ -65,7 +65,7 @@ class Navigation( val currentSection = getCurrentSection(currentDestination) TopAppBar(title = { - Text(text = currentSection.sectionTopBarName()) + currentSection.Title() }, navigationIcon = { val backButtonAnimation by animateFloatAsState(if (currentSection.canGoBack()) 1f else 0f, label = "backButtonAnimation" diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt index 1b4213659..ed1f82427 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt @@ -3,6 +3,7 @@ package me.rhunk.snapenhance.ui.manager import androidx.compose.foundation.layout.RowScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector import androidx.navigation.NavController @@ -70,6 +71,9 @@ open class Section { open fun sectionTopBarName(): String = context.translation["manager.routes.${enumSection.route}"] open fun canGoBack(): Boolean = false + @Composable + open fun Title() { Text(text = sectionTopBarName()) } + @Composable open fun Content() { NotImplemented() } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt index 96a4fd1f3..58e38a55a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt @@ -4,16 +4,15 @@ import android.content.Intent import android.net.Uri import androidx.compose.foundation.Image import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale @@ -21,7 +20,11 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -30,12 +33,11 @@ import androidx.navigation.compose.composable import androidx.navigation.navigation import kotlinx.coroutines.launch import me.rhunk.snapenhance.R +import me.rhunk.snapenhance.common.BuildConfig import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.manager.data.InstallationSummary import me.rhunk.snapenhance.ui.manager.data.Updater -import me.rhunk.snapenhance.ui.setup.Requirements import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper -import me.rhunk.snapenhance.ui.util.saveFile import java.util.Locale class HomeSection : Section() { @@ -56,31 +58,6 @@ class HomeSection : Section() { activityLauncherHelper = ActivityLauncherHelper(context.activity!!) } - @Composable - private fun SummaryCardRow(icon: ImageVector? = null, title: String, action: @Composable () -> Unit) { - Row( - modifier = Modifier.padding(all = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - icon?.let { - Icon( - imageVector = it, - contentDescription = null, - modifier = Modifier - .padding(end = 10.dp) - .align(Alignment.CenterVertically) - ) - } - Text(text = title, modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically) - ) - Column { - action() - } - } - } - @Composable private fun SummaryCards(installationSummary: InstallationSummary) { val summaryInfo = remember { @@ -130,35 +107,6 @@ class HomeSection : Section() { } } } - - OutlinedCard( - modifier = Modifier - .padding(all = cardMargin) - .fillMaxWidth() - ) { - SummaryCardRow( - icon = Icons.Filled.Map, - title = if (installationSummary.modInfo == null || installationSummary.modInfo.mappingsOutdated == true) { - "Mappings ${if (installationSummary.modInfo == null) "not generated" else "outdated"}" - } else { - "Mappings are up-to-date" - } - ) { - Button(onClick = { - context.checkForRequirements(Requirements.MAPPINGS) - }, modifier = Modifier.height(40.dp)) { - Icon(Icons.Filled.Refresh, contentDescription = null) - } - } - - SummaryCardRow(icon = Icons.Filled.Language, title = userLocale ?: "Unknown") { - Button(onClick = { - context.checkForRequirements(Requirements.LANGUAGE) - }, modifier = Modifier.height(40.dp)) { - Icon(Icons.Filled.OpenInNew, contentDescription = null) - } - } - } } override fun onResumed() { @@ -173,7 +121,9 @@ class HomeSection : Section() { context.longToast("SnapEnhance failed to load installation summary: ${it.message}") } runCatching { - latestUpdate = Updater.checkForLatestRelease() + if (!BuildConfig.DEBUG) { + latestUpdate = Updater.checkForLatestRelease() + } }.onFailure { context.longToast("SnapEnhance failed to check for updates: ${it.message}") } @@ -211,48 +161,7 @@ class HomeSection : Section() { } } LOGS_SECTION_ROUTE -> { - var showDropDown by remember { mutableStateOf(false) } - - IconButton(onClick = { - showDropDown = true - }) { - Icon(Icons.Filled.MoreVert, contentDescription = null) - } - - DropdownMenu( - expanded = showDropDown, - onDismissRequest = { showDropDown = false }, - modifier = Modifier.align(Alignment.CenterVertically) - ) { - DropdownMenuItem(onClick = { - context.log.clearLogs() - navController.navigate(LOGS_SECTION_ROUTE) - showDropDown = false - }, text = { - Text( - text = context.translation["manager.sections.home.logs.clear_logs_button"] - ) - }) - - DropdownMenuItem(onClick = { - activityLauncherHelper.saveFile("snapenhance-logs-${System.currentTimeMillis()}.zip", "application/zip") { uri -> - context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use { - runCatching { - context.log.exportLogsToZip(it) - context.longToast("Saved logs to $uri") - }.onFailure { - context.longToast("Failed to save logs to $uri!") - context.log.error("Failed to save logs to $uri!", it) - } - } - } - showDropDown = false - }, text = { - Text( - text = context.translation["manager.sections.home.logs.export_logs_button"] - ) - }) - } + homeSubSection.LogsTopBarButtons(activityLauncherHelper, navController, this) } } } @@ -279,30 +188,84 @@ class HomeSection : Section() { @Composable @Preview override fun Content() { + val avenirNextFontFamily = remember { + FontFamily( + Font(R.font.avenir_next_medium, FontWeight.Medium) + ) + } + Column( modifier = Modifier .verticalScroll(ScrollState(0)) ) { - Column( + + Image( + painter = painterResource(id = R.drawable.launcher_icon_monochrome), + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), + contentScale = ContentScale.FillHeight, + modifier = Modifier + .fillMaxWidth() + .scale(1.8f) + .height(90.dp) + ) + + Text( + text = arrayOf("\u0020", "\u0065", "\u0063", "\u006e", "\u0061", "\u0068", "\u006e", "\u0045", "\u0070", "\u0061", "\u006e", "\u0053").reversed().joinToString(""), + fontSize = 30.sp, + fontFamily = avenirNextFontFamily, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + + Text( + text = "v" + BuildConfig.VERSION_NAME + " \u00b7 by rhunk", + fontSize = 12.sp, + fontFamily = avenirNextFontFamily, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + + Text( + text = "An Xposed module made to enhance your Snapchat experience", modifier = Modifier + .padding(16.dp) .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + textAlign = TextAlign.Center, + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(15.dp, Alignment.CenterHorizontally), + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp) ) { - Image( - painter = painterResource(id = R.drawable.launcher_icon_monochrome), + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_github), contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), - contentScale = ContentScale.FillHeight, - modifier = Modifier - .height(120.dp) - .scale(1.75f) + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(32.dp).clickable { + context.activity?.startActivity( + Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse("https://github.com/rhunk/SnapEnhance") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + ) + } ) - Text( - text = arrayOf("\u0065", "\u0063", "\u006e", "\u0061", "\u0068", "\u006e", "\u0045", "\u0070", "\u0061", "\u006e", "\u0053").reversed().joinToString(""), - fontSize = 30.sp, - modifier = Modifier.padding(16.dp), + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_telegram), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(32.dp).clickable { + context.activity?.startActivity( + Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse("https://t.me/snapenhance") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + ) + } ) } + Spacer(modifier = Modifier.height(20.dp)) if (latestUpdate != null) { OutlinedCard( @@ -345,11 +308,6 @@ class HomeSection : Section() { } } - Text( - text = "An Xposed module made to enhance your Snapchat experience", - modifier = Modifier.padding(16.dp) - ) - SummaryCards(installationSummary = installationSummary ?: return) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt index f797549b4..dc537bd1c 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt @@ -1,5 +1,6 @@ package me.rhunk.snapenhance.ui.manager.sections.home +import android.net.Uri import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures @@ -10,14 +11,12 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardDoubleArrowDown import androidx.compose.material.icons.filled.KeyboardDoubleArrowUp +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Report import androidx.compose.material.icons.outlined.Warning -import androidx.compose.material3.FilledIconButton -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -28,6 +27,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.navigation.NavController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -36,8 +36,10 @@ import me.rhunk.snapenhance.LogReader import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.common.logger.LogChannel import me.rhunk.snapenhance.common.logger.LogLevel +import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper import me.rhunk.snapenhance.ui.util.pullrefresh.PullRefreshIndicator import me.rhunk.snapenhance.ui.util.pullrefresh.rememberPullRefreshState +import me.rhunk.snapenhance.ui.util.saveFile class HomeSubSection( private val context: RemoteSideContext @@ -170,6 +172,54 @@ class HomeSubSection( } } + @Composable + fun LogsTopBarButtons(activityLauncherHelper: ActivityLauncherHelper, navController: NavController, rowScope: RowScope) { + var showDropDown by remember { mutableStateOf(false) } + + IconButton(onClick = { + showDropDown = true + }) { + Icon(Icons.Filled.MoreVert, contentDescription = null) + } + + rowScope.apply { + DropdownMenu( + expanded = showDropDown, + onDismissRequest = { showDropDown = false }, + modifier = Modifier.align(Alignment.CenterVertically) + ) { + DropdownMenuItem(onClick = { + context.log.clearLogs() + navController.navigate(HomeSection.LOGS_SECTION_ROUTE) + showDropDown = false + }, text = { + Text( + text = context.translation["manager.sections.home.logs.clear_logs_button"] + ) + }) + + DropdownMenuItem(onClick = { + activityLauncherHelper.saveFile("snapenhance-logs-${System.currentTimeMillis()}.zip", "application/zip") { uri -> + context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use { + runCatching { + context.log.exportLogsToZip(it) + context.longToast("Saved logs to $uri") + }.onFailure { + context.longToast("Failed to save logs to $uri!") + context.log.error("Failed to save logs to $uri!", it) + } + } + } + showDropDown = false + }, text = { + Text( + text = context.translation["manager.sections.home.logs.export_logs_button"] + ) + }) + } + } + } + @Composable fun LogsActionButtons() { val coroutineScope = rememberCoroutineScope() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/SettingsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/SettingsSection.kt index bc0377d21..100ad4363 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/SettingsSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/SettingsSection.kt @@ -21,6 +21,7 @@ import me.rhunk.snapenhance.common.Constants import me.rhunk.snapenhance.common.action.EnumAction import me.rhunk.snapenhance.common.bridge.types.BridgeFileType import me.rhunk.snapenhance.ui.manager.Section +import me.rhunk.snapenhance.ui.setup.Requirements import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper import me.rhunk.snapenhance.ui.util.AlertDialogs import me.rhunk.snapenhance.ui.util.saveFile @@ -63,7 +64,7 @@ class SettingsSection( ShiftedRow( modifier = Modifier .fillMaxWidth() - .height(65.dp) + .height(55.dp) .clickable { takeAction() }, @@ -116,6 +117,12 @@ class SettingsSection( launchActionIntent(enumAction) } } + RowAction(title = "Regenerate Mappings") { + context.checkForRequirements(Requirements.MAPPINGS) + } + RowAction(title = "Change Language") { + context.checkForRequirements(Requirements.LANGUAGE) + } RowTitle(title = "Message Logger") ShiftedRow { Column( @@ -227,6 +234,7 @@ class SettingsSection( Text(text = "Clear") } } + Spacer(modifier = Modifier.height(50.dp)) } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml new file mode 100644 index 000000000..daa9e1018 --- /dev/null +++ b/app/src/main/res/drawable/ic_github.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_telegram.xml b/app/src/main/res/drawable/ic_telegram.xml new file mode 100644 index 000000000..ab02ef56f --- /dev/null +++ b/app/src/main/res/drawable/ic_telegram.xml @@ -0,0 +1,4 @@ + + + diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/action/EnumAction.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/action/EnumAction.kt index b7aa535b3..e31ea2594 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/action/EnumAction.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/action/EnumAction.kt @@ -7,10 +7,10 @@ enum class EnumAction( val exitOnFinish: Boolean = false, val isCritical: Boolean = false, ) { - CLEAN_CACHE("clean_snapchat_cache", exitOnFinish = true), EXPORT_CHAT_MESSAGES("export_chat_messages"), EXPORT_MEMORIES("export_memories"), - BULK_MESSAGING_ACTION("bulk_messaging_action"); + BULK_MESSAGING_ACTION("bulk_messaging_action"), + CLEAN_CACHE("clean_snapchat_cache", exitOnFinish = true); companion object { const val ACTION_PARAMETER = "se_action" From 1241d68d3cacb114540b0624612ca4f0855a5bf9 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Wed, 3 Jan 2024 01:48:30 +0100 Subject: [PATCH 33/53] fix(app/ui): error handling --- .../manager/sections/social/LoggedStories.kt | 41 ++++++++++------- .../sections/social/MessagingPreview.kt | 45 ++++++++++--------- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/LoggedStories.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/LoggedStories.kt index ce83fb743..f5a67bb11 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/LoggedStories.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/LoggedStories.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider import coil.annotation.ExperimentalCoilApi @@ -161,6 +162,10 @@ fun LoggedStories( } } + if (stories.isEmpty()) { + Text(text = "No stories found", Modifier.fillMaxWidth(), textAlign = TextAlign.Center) + } + LazyVerticalGrid( columns = GridCells.Adaptive(100.dp), contentPadding = PaddingValues(8.dp), @@ -203,25 +208,29 @@ fun LoggedStories( return@withTimeout } - val response = httpClient.newCall(Request( - url = story.url.toHttpUrl() - )).execute() - response.body.byteStream().use { - val decrypted = story.key?.let { _ -> - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(story.key, "AES"), IvParameterSpec(story.iv)) - CipherInputStream(it, cipher) - } ?: it + runCatching { + val response = httpClient.newCall(Request( + url = story.url.toHttpUrl() + )).execute() + response.body.byteStream().use { + val decrypted = story.key?.let { _ -> + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(story.key, "AES"), IvParameterSpec(story.iv)) + CipherInputStream(it, cipher) + } ?: it - context.imageLoader.diskCache?.openEditor(uniqueHash)?.apply { - data.toFile().outputStream().use { fos -> - decrypted.copyTo(fos) - } - commitAndOpenSnapshot()?.use { snapshot -> - openDiskCacheSnapshot(snapshot) - snapshot.close() + context.imageLoader.diskCache?.openEditor(uniqueHash)?.apply { + data.toFile().outputStream().use { fos -> + decrypted.copyTo(fos) + } + commitAndOpenSnapshot()?.use { snapshot -> + openDiskCacheSnapshot(snapshot) + snapshot.close() + } } } + }.onFailure { + context.log.error("Failed to load story", it) } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/MessagingPreview.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/MessagingPreview.kt index 2a8175530..870f20a23 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/MessagingPreview.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/MessagingPreview.kt @@ -423,29 +423,34 @@ class MessagingPreview( } private fun onMessagingBridgeReady() { - messagingBridge = context.bridgeService!!.messagingBridge!! - conversationId = if (scope == SocialScope.FRIEND) messagingBridge.getOneToOneConversationId(scopeId) else scopeId - if (conversationId == null) { - context.longToast("Failed to fetch conversation id") - return - } - if (!messagingBridge.isSessionStarted) { - context.androidContext.packageManager.getLaunchIntentForPackage( - Constants.SNAPCHAT_PACKAGE_NAME - )?.let { - val mainIntent = Intent.makeRestartActivityTask(it.component).apply { - putExtra(ReceiversConfig.MESSAGING_PREVIEW_EXTRA, true) - } - context.androidContext.startActivity(mainIntent) + runCatching { + messagingBridge = context.bridgeService!!.messagingBridge!! + conversationId = if (scope == SocialScope.FRIEND) messagingBridge.getOneToOneConversationId(scopeId) else scopeId + if (conversationId == null) { + context.longToast("Failed to fetch conversation id") + return } - messagingBridge.registerSessionStartListener(object: SessionStartListener.Stub() { - override fun onConnected() { - fetchNewMessages() + if (!messagingBridge.isSessionStarted) { + context.androidContext.packageManager.getLaunchIntentForPackage( + Constants.SNAPCHAT_PACKAGE_NAME + )?.let { + val mainIntent = Intent.makeRestartActivityTask(it.component).apply { + putExtra(ReceiversConfig.MESSAGING_PREVIEW_EXTRA, true) + } + context.androidContext.startActivity(mainIntent) } - }) - return + messagingBridge.registerSessionStartListener(object: SessionStartListener.Stub() { + override fun onConnected() { + fetchNewMessages() + } + }) + return + } + fetchNewMessages() + }.onFailure { + context.longToast("Failed to initialize messaging bridge") + context.log.error("Failed to initialize messaging bridge", it) } - fetchNewMessages() } @Composable From 83fd108af9212a75a34ad15094362545ee80b296 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 4 Jan 2024 01:36:03 +0100 Subject: [PATCH 34/53] fix: metrics --- .../kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt | 2 +- .../snapenhance/core/features/impl/global/DisableMetrics.kt | 4 ++-- .../main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt index c74d00338..37a0cf71f 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt @@ -11,7 +11,7 @@ class Global : ConfigContainer() { val suspendLocationUpdates = boolean("suspend_location_updates") { requireRestart() } val snapchatPlus = boolean("snapchat_plus") { requireRestart() } val disableConfirmationDialogs = multiple("disable_confirmation_dialogs", "remove_friend", "block_friend", "ignore_friend", "hide_friend", "hide_conversation", "clear_conversation") { requireRestart() } - val disableMetrics = boolean("disable_metrics") + val disableMetrics = boolean("disable_metrics") { requireRestart() } val disablePublicStories = boolean("disable_public_stories") { requireRestart(); requireCleanCache() } val blockAds = boolean("block_ads") val spotlightCommentsUsername = boolean("spotlight_comments_username") { requireRestart() } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMetrics.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMetrics.kt index a8ec51418..be532061f 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMetrics.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMetrics.kt @@ -6,9 +6,9 @@ import me.rhunk.snapenhance.core.features.FeatureLoadParams class DisableMetrics : Feature("DisableMetrics", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { - val disableMetrics by context.config.global.disableMetrics + if (!context.config.global.disableMetrics.get()) return - context.event.subscribe(NetworkApiRequestEvent::class, { disableMetrics }) { param -> + context.event.subscribe(NetworkApiRequestEvent::class) { param -> val url = param.url if (url.contains("app-analytics") || url.endsWith("metrics")) { param.canceled = true diff --git a/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt b/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt index 7385eecf6..110d35b9f 100644 --- a/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt +++ b/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt @@ -31,7 +31,7 @@ class NativeLib { }.onFailure { Log.e("SnapEnhance", "nativeUnaryCallCallback failed", it) } - if (!nativeRequestData.buffer.contentEquals(buffer) || nativeRequestData.canceled) return nativeRequestData + if (nativeRequestData.canceled || !nativeRequestData.buffer.contentEquals(buffer)) return nativeRequestData return null } From f7833181986dc722c00cf0df2b932be74ad6899f Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 4 Jan 2024 02:24:49 +0100 Subject: [PATCH 35/53] fix(app/streaks_reminder): negative time --- .../me/rhunk/snapenhance/messaging/StreaksReminder.kt | 6 +++--- .../rhunk/snapenhance/common/data/MessagingCoreObjects.kt | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt index 0c1ba5e49..dc20eab4b 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt @@ -44,14 +44,14 @@ class StreaksReminder( if (streaksReminderConfig.globalState != true) return - val interval = streaksReminderConfig.interval.get() + val interval = streaksReminderConfig.interval.get().hours val remainingHours = streaksReminderConfig.remainingHours.get() - if (sharedPreferences.getLong("lastStreaksReminder", 0).milliseconds + interval.hours - 10.minutes > System.currentTimeMillis().milliseconds) return + if (sharedPreferences.getLong("lastStreaksReminder", 0).milliseconds + interval - 10.minutes > System.currentTimeMillis().milliseconds) return sharedPreferences.edit().putLong("lastStreaksReminder", System.currentTimeMillis()).apply() remoteSideContext.androidContext.getSystemService(AlarmManager::class.java).setRepeating( - AlarmManager.RTC_WAKEUP, 5000, interval.toLong() * 60 * 60 * 1000, + AlarmManager.RTC_WAKEUP, 5000, interval.inWholeMilliseconds, PendingIntent.getBroadcast(remoteSideContext.androidContext, 0, Intent(remoteSideContext.androidContext, StreaksReminder::class.java), PendingIntent.FLAG_IMMUTABLE) ) diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/MessagingCoreObjects.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/MessagingCoreObjects.kt index fbd6d2f85..ce55e5582 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/MessagingCoreObjects.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/MessagingCoreObjects.kt @@ -2,6 +2,7 @@ package me.rhunk.snapenhance.common.data import me.rhunk.snapenhance.common.config.FeatureNotice import me.rhunk.snapenhance.common.util.SerializableDataObject +import kotlin.time.Duration.Companion.hours enum class RuleState( @@ -59,7 +60,9 @@ data class FriendStreaks( ) : SerializableDataObject() { fun hoursLeft() = (expirationTimestamp - System.currentTimeMillis()) / 1000 / 60 / 60 - fun isAboutToExpire(expireHours: Int) = expirationTimestamp - System.currentTimeMillis() < expireHours * 60 * 60 * 1000 + fun isAboutToExpire(expireHours: Int) = (expirationTimestamp - System.currentTimeMillis()).let { + it > 0 && it < expireHours.hours.inWholeMilliseconds + } } data class MessagingGroupInfo( From 49bc15cf1bd0b204a2c232304059c2ea036594a0 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 4 Jan 2024 03:33:17 +0100 Subject: [PATCH 36/53] chore: merge translations (#529) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: metrics * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: SnapEnhance/app Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ * Translated using Weblate (Arabic (Saudi Arabia)) Currently translated at 100.0% (574 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ar_SA/ * Translated using Weblate (Arabic (Saudi Arabia)) Currently translated at 100.0% (574 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ar_SA/ * Translated using Weblate (Arabic (Saudi Arabia)) Currently translated at 100.0% (574 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ar_SA/ * Translated using Weblate (French) Currently translated at 51.7% (297 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/fr/ * Translated using Weblate (German) Currently translated at 8.0% (46 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/de/ * Translated using Weblate (Hindi) Currently translated at 8.0% (46 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/hi/ * Translated using Weblate (Hungarian) Currently translated at 8.0% (46 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/hu/ * Translated using Weblate (Italian) Currently translated at 8.0% (46 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/it/ * Translated using Weblate (Turkish) Currently translated at 8.0% (46 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/tr/ * Added translation using Weblate (Danish) * Translated using Weblate (Danish) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/da/ * Added translation using Weblate (Bengali) * Added translation using Weblate (Finnish) * Added translation using Weblate (Gujarati (India)) * Added translation using Weblate (Hungarian (hu_ZZ)) * Added translation using Weblate (Indonesian) * Added translation using Weblate (Italian (it_V26)) * Added translation using Weblate (Kurdish) * Added translation using Weblate (Dutch) * Added translation using Weblate (Polish) * Added translation using Weblate (Portuguese) * Added translation using Weblate (Romanian) * Added translation using Weblate (Russian) * Added translation using Weblate (Swedish) * Added translation using Weblate (Urdu (India)) * Added translation using Weblate (Chinese (Simplified) (zh_SIMPLIFIED)) * Translated using Weblate (Bengali) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/bn/ * Translated using Weblate (Chinese (Simplified) (zh_SIMPLIFIED)) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/zh_SIMPLIFIED/ * Translated using Weblate (Dutch) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/nl/ * Translated using Weblate (Finnish) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/fi/ * Translated using Weblate (Gujarati (India)) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/gu_IN/ * Translated using Weblate (Hungarian (hu_ZZ)) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/hu_ZZ/ * Translated using Weblate (Indonesian) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/id/ * Translated using Weblate (Kurdish) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ku/ * Translated using Weblate (Polish) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/pl/ * Translated using Weblate (Portuguese) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/pt/ * Translated using Weblate (Romanian) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ro/ * Translated using Weblate (Russian) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ru/ * Translated using Weblate (Swedish) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/sv/ * Translated using Weblate (Urdu (India)) Currently translated at 0.0% (0 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ur_IN/ * Deleted translation using Weblate (Italian (it_V26)) * Deleted translation using Weblate (Hungarian (hu_ZZ)) * Update translation files Updated by "Remove blank strings" hook in Weblate. Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ * Translated using Weblate (Arabic (Saudi Arabia)) Currently translated at 65.6% (377 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ar_SA/ * Update translation files Updated by "Remove blank strings" hook in Weblate. Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ * Translated using Weblate (German) Currently translated at 65.5% (376 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/de/ * Translated using Weblate (French) Currently translated at 58.7% (337 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/fr/ * Translated using Weblate (Hindi) Currently translated at 11.8% (68 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/hi/ * Translated using Weblate (Hungarian) Currently translated at 51.3% (295 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/hu/ * Translated using Weblate (Danish) Currently translated at 65.1% (374 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/da/ * Translated using Weblate (Gujarati (India)) Currently translated at 0.3% (2 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/gu_IN/ * Translated using Weblate (Polish) Currently translated at 12.0% (69 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/pl/ * Translated using Weblate (Swedish) Currently translated at 4.7% (27 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/sv/ * Translated using Weblate (Urdu (India)) Currently translated at 65.6% (377 of 574 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ur_IN/ * Update translation files Updated by "Remove blank strings" hook in Weblate. Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ * Translated using Weblate (German) Currently translated at 71.5% (412 of 576 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/de/ * Translated using Weblate (German) Currently translated at 72.5% (418 of 576 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/de/ * Translated using Weblate (German) Currently translated at 100.0% (578 of 578 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/de/ * Translated using Weblate (Turkish) Currently translated at 65.3% (378 of 578 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/tr/ * Added translation using Weblate (Norwegian Bokmål) * Translated using Weblate (Turkish) Currently translated at 100.0% (578 of 578 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/tr/ * Translated using Weblate (Dutch) Currently translated at 20.7% (120 of 578 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/nl/ * Translated using Weblate (Norwegian Bokmål) Currently translated at 6.4% (37 of 578 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/nb_NO/ * Update translation files Updated by "Remove blank strings" hook in Weblate. Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/ * Translated using Weblate (Turkish) Currently translated at 100.0% (587 of 587 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/tr/ * Translated using Weblate (Turkish) Currently translated at 99.8% (601 of 602 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/tr/ * Translated using Weblate (Turkish) Currently translated at 99.8% (603 of 604 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/tr/ * Translated using Weblate (Hungarian) Currently translated at 51.4% (311 of 604 strings) Translation: SnapEnhance/SnapEnhance Translate-URL: https://hosted.weblate.org/projects/snapenhance/app/hu/ * fix: conflict --------- Co-authored-by: rhunk <101876869+rhunk@users.noreply.github.com> Co-authored-by: auth <64337177+authorisation@users.noreply.github.com> Co-authored-by: L3N0X Co-authored-by: Caner Karaca Co-authored-by: Allan Nordhøy Co-authored-by: IrepY --- common/src/main/assets/lang/ar_SA.json | 915 +++++++----- common/src/main/assets/lang/bn.json | 42 + common/src/main/assets/lang/da.json | 699 +++++++++ common/src/main/assets/lang/de_DE.json | 1284 +++++++++++----- common/src/main/assets/lang/fi.json | 702 +++++++++ common/src/main/assets/lang/fr_FR.json | 636 ++++---- common/src/main/assets/lang/gu_IN.json | 8 + common/src/main/assets/lang/hi_IN.json | 419 +----- common/src/main/assets/lang/hu_HU.json | 858 ++++++----- common/src/main/assets/lang/id.json | 40 + common/src/main/assets/lang/it_IT.json | 385 +---- common/src/main/assets/lang/ku.json | 76 + common/src/main/assets/lang/nb_NO.json | 106 ++ common/src/main/assets/lang/nl.json | 247 ++++ common/src/main/assets/lang/pl.json | 112 ++ common/src/main/assets/lang/pt.json | 38 + common/src/main/assets/lang/ro.json | 53 + common/src/main/assets/lang/ru.json | 16 + common/src/main/assets/lang/sv.json | 50 + common/src/main/assets/lang/tr_TR.json | 1308 ++++++++++++----- common/src/main/assets/lang/ur_IN.json | 702 +++++++++ .../src/main/assets/lang/zh_SIMPLIFIED.json | 55 + .../features/impl/global/DisableMetrics.kt | 2 +- 23 files changed, 6295 insertions(+), 2458 deletions(-) create mode 100644 common/src/main/assets/lang/bn.json create mode 100644 common/src/main/assets/lang/da.json create mode 100644 common/src/main/assets/lang/fi.json create mode 100644 common/src/main/assets/lang/gu_IN.json create mode 100644 common/src/main/assets/lang/id.json create mode 100644 common/src/main/assets/lang/ku.json create mode 100644 common/src/main/assets/lang/nb_NO.json create mode 100644 common/src/main/assets/lang/nl.json create mode 100644 common/src/main/assets/lang/pl.json create mode 100644 common/src/main/assets/lang/pt.json create mode 100644 common/src/main/assets/lang/ro.json create mode 100644 common/src/main/assets/lang/ru.json create mode 100644 common/src/main/assets/lang/sv.json create mode 100644 common/src/main/assets/lang/ur_IN.json create mode 100644 common/src/main/assets/lang/zh_SIMPLIFIED.json diff --git a/common/src/main/assets/lang/ar_SA.json b/common/src/main/assets/lang/ar_SA.json index 5536169fe..19679934f 100644 --- a/common/src/main/assets/lang/ar_SA.json +++ b/common/src/main/assets/lang/ar_SA.json @@ -1,285 +1,565 @@ { - "category": { - "spying_privacy": "التخفي و الخصوصية", - "media_manager": "إدارة الوسائط", - "ui_tweaks": "الواجهة و الأدوات", - "camera": "الكاميرا", - "updates": "التحديثات", - "experimental_debugging": "تجريبي" + "setup": { + "dialogs": { + "select_language": "تحديد اللغة", + "save_folder": "يتطلب SnapEnhance أذونات التخزين لتنزيل الوسائط وحفظها من Snapchat.\nالرجاء اختيار المكان الذي سيتم تنزيل الوسائط إليه.", + "select_save_folder_button": "تحديد المجلد" + }, + "mappings": { + "dialog": "لدعم مجموعة واسعة من إصدارات Snapchat ديناميكيًا، تعد التعيينات ضرورية لكي يعمل SnapEnhance بشكل صحيح، ويجب ألا يستغرق ذلك أكثر من 5 ثوانٍ.", + "generate_button": "إنشاء", + "generate_failure_no_snapchat": "لم يتمكن SnapEnhance من اكتشاف Snapchat، يرجى محاولة إعادة تثبيت Snapchat.", + "generate_failure": "حدث خطأ أثناء محاولة إنشاء التعيينات، الرجاء المحاولة مرة أخرى.", + "generate_success": "تم إنشاء التعيينات بنجاح." + }, + "permissions": { + "dialog": "للمتابعة، يجب أن تستوفي المتطلبات التالية:", + "notification_access": "الوصول للإشعارات", + "battery_optimization": "تحسين البطارية", + "display_over_other_apps": "عرض فوق التطبيقات الأخرى", + "request_button": "طلب" + } + }, + "manager": { + "routes": { + "features": "الخصائص", + "home": "الصفحة الرئيسية", + "home_settings": "الإعدادات", + "home_logs": "السجلات", + "social": "وسائل التواصل", + "scripts": "السكريبتات" + }, + "sections": { + "home": { + "logs": { + "clear_logs_button": "مَسح السجلات", + "export_logs_button": "تصدير السجلات" + } + }, + "features": { + "disabled": "معطّل" + }, + "social": { + "e2ee_title": "تشفير End-to-End", + "rules_title": "القواعد", + "participants_text": "{count} مشارِكين", + "not_found": "لم يتم العثور عليه", + "streaks_title": "Streaks", + "streaks_length_text": "الطول: {length}", + "streaks_expiration_short": "{hours}ساعة", + "streaks_expiration_text": "تنتهي الصَّلاحِيَة في {eta}", + "reminder_button": "تعيين تذكير" + } + }, + "dialogs": { + "add_friend": { + "title": "إضافة صديق أو مجموعة", + "search_hint": "البحث", + "fetch_error": "فشل في جلب البيانات", + "category_groups": "المجموعات", + "category_friends": "الأصدقاء" + } + } + }, + "rules": { + "modes": { + "blacklist": "وضع القائمة السوداء", + "whitelist": "وضع القائمة البيضاء" + }, + "properties": { + "auto_download": { + "name": "التنزيل التلقائي", + "description": "تنزيل السنابات تلقائيًا عند مشاهدتها", + "options": { + "blacklist": "استبعاد من التنزيل التلقائي", + "whitelist": "تنزيل تلقائي" + } + }, + "stealth": { + "name": "وضع التخفي", + "description": "يمنع أي شخص من معرفة أنك قمت بفتح السنابات/الدردشات والمحادثات الخاصة به", + "options": { + "blacklist": "استبعاد من وضع التخفي", + "whitelist": "وضع التخفي" + } + }, + "auto_save": { + "name": "حفظ تلقائي", + "description": "يحفظ رسائل الدردشة عند مشاهدتها", + "options": { + "blacklist": "استبعاد من الحفظ التلقائي", + "whitelist": "الحفظ التلقائي" + } + }, + "hide_friend_feed": { + "name": "إخفاء من موجز الأصدقاء" + }, + "e2e_encryption": { + "name": "استخدام تشفير E2E" + }, + "pin_conversation": { + "name": "تثبيت المحادثة" + } + } }, - "action": { - "clean_cache": "تنظيف ذاكرة التخزين المؤقت", + "actions": { + "clean_snapchat_cache": "تنظيف ذاكرة تخزين Snapchat المؤقتة", "clear_message_logger": "مسح مسجّل الرسائل", "refresh_mappings": "إعادة تحميل الأداة", "open_map": "إختيار الموقع على الخريطة", "check_for_updates": "التحقق من توفر تحديثات", "export_chat_messages": "تصدير رسائل الدردشة" }, - "property": { - "message_logger": { - "name": "تسجيل الرسائل", - "description": "يمنع حذف الرسائل" - }, - "prevent_read_receipts": { - "name": "منع إيصالات القراءة", - "description": "يمنع أي شخص من معرفة أنك فتحت السنابات الخاصة به" - }, - "hide_bitmoji_presence": { - "name": "إخفاء وجود Bitmoji", - "description": "يخفي وجود Bitmoji الخاص بك من الدردشة" - }, - "better_notifications": { - "name": "إشعارات أفضل", - "description": "يعرض المزيد من المعلومات في الإشعارات" - }, - "notification_blacklist": { - "name": "الإشعارات المحظورة", - "description": "يخفي نوع إشعار محدد" - }, - "disable_metrics": { - "name": "تعطيل المقاييس", - "description": "يعطّل المقاييس المرسلة إلى Snapchat" - }, - "block_ads": { - "name": "حجب الإعلانات", - "description": "يمنع الإعلانات من الظهور" - }, - "unlimited_snap_view_time": { - "name": "عرض السنابة لوقت غير محدود", - "description": "يزيل الحد الزمني لعرض السنابات" - }, - "prevent_sending_messages": { - "name": "منع إرسال الرسائل", - "description": "يمنع إرسال أنواع معينة من الرسائل" - }, - "anonymous_story_view": { - "name": "مشاهدة القصة كمجهول", - "description": "يمنع أي شخص من معرفة أنك رأيت قصته" - }, - "hide_typing_notification": { - "name": "إخفاء إشعار جارٍ الكتابة", - "description": "يمنع إرسال إشعارات الكتابة" - }, - "save_folder": { - "name": "مجلد الحفظ", - "description": "المجلد الذي يتم فيه حفظ جميع الوسائط" - }, - "auto_download_options": { - "name": "خيارات التنزيل التلقائي", - "description": "حدد الوسائط التي سيتم تنزيلها تلقائيًا" - }, - "download_options": { - "name": "خيارات التنزيل", - "description": "تحديد تنسيق مسار الملَفّ" - }, - "chat_download_context_menu": { - "name": "قائمة سياق التنزيل بالدردشة", - "description": "تمكين قائمة التنزيل في الدردشة" - }, - "gallery_media_send_override": { - "name": "تجاوز إرسال الوسائط المرسلة من المعرض", - "description": "يرسل وسائط المعرض كسنابة مباشرة" - }, - "auto_save_messages": { - "name": "حفظ الرسائل تلقائيًا", - "description": "تحديد نوع الرسائل التي سيتم حفظها تلقائيًا" - }, - "force_media_source_quality": { - "name": "فرض جودة مصدر الوسائط", - "description": "يتجاوز جودة مصدر الوسائط" - }, - "download_logging": { - "name": "تنزيل السجل", - "description": "عرض إشعار عند تنزيل الوسائط" - }, - "enable_friend_feed_menu_bar": { - "name": "شريط قائمة موجز الأصدقاء", - "description": "يمكّن شريط قائمة موجز الأصدقاء الجديد" - }, - "friend_feed_menu_buttons": { - "name": "أزرار قائمة موجز الأصدقاء", - "description": "حدد الأزرار التي تريد عرضها في شريط قائمة موجز الأصدقاء" - }, - "friend_feed_menu_buttons_position": { - "name": "ترتيب موضع أزرار موجز الأصدقاء", - "description": "موضع أزرار قائمة موجز الأصدقاء" - }, - "hide_ui_elements": { - "name": "إخفاء عناصر واجهة المستخدم", - "description": "تحديد عناصر واجهة المستخدم التي تريد إخفاءها" - }, - "hide_story_section": { - "name": "إخفاء قسم القصص", - "description": "إخفاء بعض عناصر واجهة المستخدم المعروضة في قسم القصص" - }, - "story_viewer_override": { - "name": "تجاوز عارض القصة", - "description": "يقوم بتشغيل ميزات معينة أخفاها Snapchat" - }, - "streak_expiration_info": { - "name": "عرض معلومات انتهاء صَلاحِيَة سلسلة اللقطات المتتالية", - "description": "يعرض معلومات انتهاء صَلاحِيَة سلسلة اللقطات المتتالية بجانب اللقطات" - }, - "disable_snap_splitting": { - "name": "تعطيل تقسيم السنابة", - "description": "يمنع السنابات من الانقسام إلى أجزاء متعددة" - }, - "disable_video_length_restriction": { - "name": "تعطيل قيود طول الفيديو", - "description": "يعطّل قيود طول الفيديو" - }, - "snapchat_plus": { - "name": "سناب شات+", - "description": "يمكّن ميزات Snapchat Plus" - }, - "new_map_ui": { - "name": "واجهة الخريطة الجديدة", - "description": "يمكّن واجهة الخرائط الجديدة" - }, - "location_spoof": { - "name": "تزوير الموقع بخريطة السناب", - "description": "يقوم بتغيير موقعك في خريطة السناب" - }, - "message_preview_length": { - "name": "طول معاينة الرسالة", - "description": "تحديد كَمّيَّة الرسائل التي سيتم معاينتها" - }, - "unlimited_conversation_pinning": { - "name": "تثبيت محادثة غير محدود", - "description": "تمكن من القدرة على تثبيت محادثات غير محدودة" - }, - "disable_spotlight": { - "name": "تعطيل منصة الأضواء", - "description": "يعطّل صفحة منصة الأضواء" - }, - "enable_app_appearance": { - "name": "تمكين إعدادات مظهر التطبيق", - "description": "لتمكين إعدادات مظهر التطبيق المخفية" - }, - "startup_page_override": { - "name": "تجاوز صفحة بَدْء التشغيل", - "description": "يتجاوز صفحة بَدْء التشغيل" - }, - "disable_google_play_dialogs": { - "name": "تعطيل مربعات حوار خدمات Google Play", - "description": "يمنع ظهور مربعات حوار توفر خدمات Google Play" - }, - "auto_updater": { - "name": "التحديث التلقائي", - "description": "الفاصل الزمني للتحقق من توفر تحديثات" - }, - "disable_camera": { - "name": "تعطيل الكاميرا", - "description": "يمنع Snapchat من القدرة على استخدام الكاميرا" - }, - "immersive_camera_preview": { - "name": "عرض الكاميرا المغمورة", - "description": "يمنع Snapchat من اقتصاص عرض الكاميرا" - }, - "preview_resolution": { - "name": "دِّقَّة العرض", - "description": "يتجاوز دِقَّة عرض الكاميرا" - }, - "picture_resolution": { - "name": "دِقَّة الصورة", - "description": "يتجاوز دِقَّة الصورة" - }, - "force_highest_frame_rate": { - "name": "فرض أعلى معدل إطارات", - "description": "يفرض أعلى معدل إطارات ممكن" - }, - "force_camera_source_encoding": { - "name": "فرض ترميز مصدر الكاميرا", - "description": "يفرض ترميز مصدر الكاميرا" - }, - "app_passcode": { - "name": "تعيين رمز مرور التطبيق", - "description": "يعيّن رمز مرور لقفل التطبيق" - }, - "app_lock_on_resume": { - "name": "قفل التطبيق عند الإستئناف", - "description": "يقفل التطبيق عند إعادة فتحه" - }, - "infinite_story_boost": { - "name": "تعزيز القصّة بلا حدود", - "description": "يعزز قصتك بلا حدود" - }, - "meo_passcode_bypass": { - "name": "تجاوز رمز مرور خاصية خاص بي فقط", - "description": "تجاوز رمز مرور خاصية خاص بي فقط\nلن يعمل هذا إلا إذا تم إدخال رمز المرور بشكل صحيح من قبل" - }, - "amoled_dark_mode": { - "name": "الوضع الداكن AMOLED", - "description": "يمكّن وضع AMOLED الداكن\nتأكد من تمكين الوضع المظلم في إعدادات Snapchat" - }, - "unlimited_multi_snap": { - "name": "سناب متعدد غير محدود", - "description": "يسمح لك بأخذ كمية غير محدودة من السنابات المتعددة" - }, - "device_spoof": { - "name": "خداع قِيَم الجهاز", - "description": "محاكاة لقيم الأجهزة" - }, - "device_fingerprint": { - "name": "بصمة الجهاز", - "description": "Spoofs the device fingerprint" + "features": { + "notices": { + "unstable": "⚠ غير مستقر", + "ban_risk": "⚠ قد تسبب هذه الميزة حظرًا", + "internal_behavior": "⚠ قد يؤدي هذا إلى كسر السلوك الداخلي في Snapchat", + "require_native_hooks": "⚠ تتطلب هذه الميزة استخدام خطافات أصلية تجريبية لتعمل بشكل صحيح" + }, + "properties": { + "downloader": { + "name": "أداة التنزيل", + "description": "تنزيل وسائط Snapchat", + "properties": { + "save_folder": { + "name": "مجلد الحفظ", + "description": "تحديد المجلد الذي سيتم تنزيل جميع الوسائط إليه" + }, + "auto_download_sources": { + "name": "مصادر التنزيل التلقائي", + "description": "حدد المصادر للتنزيل منها تلقائيًا" + }, + "prevent_self_auto_download": { + "name": "منع التنزيل التلقائي الذاتي", + "description": "يمنع تنزيل السنابات الخاصة بك تلقائيًا" + }, + "path_format": { + "name": "تنسيق المسار", + "description": "تحديد تنسيق مسار الملَفّ" + }, + "allow_duplicate": { + "name": "السماح بالتكرار", + "description": "يسمح بتنزيل نفس الوسائط عدة مرات" + }, + "merge_overlays": { + "name": "دمج التراكبات", + "description": "يجمع النص والوسائط الخاصة بالـ Snap في ملف واحد" + }, + "force_image_format": { + "name": "فرض تنسيق الصورة", + "description": "يفرض حفظ الصور بتنسيق محدد" + }, + "force_voice_note_format": { + "name": "فرض تنسيق الملاحظة الصوتية", + "description": "يفرض حفظ الملاحظات الصوتية بتنسيق محدد" + }, + "download_profile_pictures": { + "name": "تنزيل الصور الشخصية", + "description": "يسمح لك بتنزيل صور الملف الشخصي من صفحة الملف الشخصي" + }, + "chat_download_context_menu": { + "name": "قائمة سياق التنزيل بالدردشة", + "description": "يسمح لك بتنزيل الوسائط من محادثة عن طريق الضغط المطول عليها" + }, + "ffmpeg_options": { + "name": "خيارات FFmpeg", + "description": "تحديد خيارات FFmpeg الإضافية", + "properties": { + "threads": { + "name": "المواضيع", + "description": "كمية المواضيع المراد استخدامها" + }, + "preset": { + "name": "إعداد مسبق", + "description": "تعيين سرعة التحويل" + }, + "constant_rate_factor": { + "name": "عامل المعدل الثابت", + "description": "تعيين عامل المعدل الثابت لترميز الفيديو\nمن 0 إلى 51 لـ libx264" + }, + "video_bitrate": { + "name": "معدل بث الفيديو", + "description": "تعيين معدل بث الفيديو (كيلو بايت)" + }, + "audio_bitrate": { + "name": "معدل بث الصوت", + "description": "تعيين معدل بث الصوت (كيلو بايت)" + }, + "custom_video_codec": { + "name": "ترميز فيديو مخصص", + "description": "تعيين ترميز فيديو مخصص (مثل libx264)" + }, + "custom_audio_codec": { + "name": "ترميز الصوت المخصص", + "description": "تعيين ترميز صوت مخصص (مثل AAC)" + } + } + }, + "logging": { + "name": "تسجيل", + "description": "عرض إشعار عند تنزيل الوسائط" + } + } + }, + "user_interface": { + "name": "واجهة المستخدم", + "description": "تغيير شكل ومظهر Snapchat", + "properties": { + "enable_app_appearance": { + "name": "تمكين إعدادات مظهر التطبيق", + "description": "لتمكين إعدادات مظهر التطبيق المخفية\nقد لا تكون مطلوبة على إصدارات Snapchat الأحدث" + }, + "amoled_dark_mode": { + "name": "الوضع الداكن AMOLED", + "description": "يمكّن وضع AMOLED الداكن\nتأكد من تمكين الوضع المظلم في إعدادات Snapchat" + }, + "friend_feed_message_preview": { + "name": "معاينة رسالة موجز الأصدقاء", + "description": "يعرض معاينة لآخر الرسائل في موجز الأصدقاء", + "properties": { + "amount": { + "name": "الكمية", + "description": "تحديد كَمّيَّة الرسائل المراد معاينتها" + } + } + }, + "bootstrap_override": { + "name": "تجاوز Bootstrap", + "description": "يتجاوز إعدادات التمهيد لواجهة المستخدم", + "properties": { + "app_appearance": { + "name": "مظهر التطبيق", + "description": "يضبط مظهر التطبيق المستمر" + }, + "home_tab": { + "name": "علامة التبويب الصفحة الرئيسية", + "description": "يتجاوز علامة تبويب بدء التشغيل عند فتح Snapchat" + } + } + }, + "map_friend_nametags": { + "name": "علامات أسماء خريطة الأصدقاء المحسنة", + "description": "تحسين علامات أسماء الأصدقاء على خريطة Snapmap" + }, + "streak_expiration_info": { + "name": "عرض معلومات انتهاء صَلاحِيَة سلسلة اللقطات المتتالية", + "description": "يعرض مؤقت انتهاء سلسلة التسلسل بالقرب من عداد المتسلسلات" + }, + "hide_friend_feed_entry": { + "name": "إخفاء إدخال موجز الأصدقاء", + "description": "إخفاء صديق محدد من موجز الأصدقاء\nاستخدم علامة التبويب الاجتماعية لإدارة هذه الميزة" + }, + "hide_streak_restore": { + "name": "إخفاء استعادة Streak", + "description": "إخفاء زر الاستعادة في موجز الأصدقاء" + }, + "hide_story_sections": { + "name": "إخفاء قسم القصص", + "description": "إخفاء بعض عناصر واجهة المستخدم المعروضة في قسم القصص" + }, + "hide_ui_components": { + "name": "إخفاء مكونات واجهة المستخدم", + "description": "حدد مكونات واجهة المستخدم التي تريد إخفاءها" + }, + "disable_spotlight": { + "name": "تعطيل منصة الأضواء", + "description": "يعطّل صفحة منصة الأضواء" + }, + "friend_feed_menu_buttons": { + "name": "أزرار قائمة موجز الأصدقاء", + "description": "حدد الأزرار التي تريد عرضها في شريط قائمة موجز الأصدقاء" + }, + "friend_feed_menu_position": { + "name": "ترتيب موضع موجز الأصدقاء", + "description": "موضع مكون قائمة موجز الأصدقاء" + }, + "enable_friend_feed_menu_bar": { + "name": "شريط قائمة موجز الأصدقاء", + "description": "يمكّن شريط قائمة موجز الأصدقاء الجديد" + } + } + }, + "messaging": { + "name": "المراسلة", + "description": "تغيير كيفية تفاعلك مع الأصدقاء", + "properties": { + "anonymous_story_viewing": { + "name": "مشاهدة القصة كمجهول", + "description": "يمنع أي شخص من معرفة أنك رأيت قصته" + }, + "hide_bitmoji_presence": { + "name": "إخفاء وجود Bitmoji", + "description": "يمنع ظهور Bitmoji الخاص بك أثناء الدردشة" + }, + "hide_typing_notifications": { + "name": "إخفاء إشعار جارٍ الكتابة", + "description": "يمنع أي شخص من معرفة أنك تكتب رسالة" + }, + "unlimited_snap_view_time": { + "name": "عرض السنابة لوقت غير محدود", + "description": "إزالة الحد الزمني لعرض السنابات" + }, + "disable_replay_in_ff": { + "name": "تعطيل إعادة التشغيل في FF", + "description": "تعطيل القدرة على إعادة التشغيل بالضغط المطول من موجز الأصدقاء" + }, + "message_preview_length": { + "name": "طول معاينة الرسالة", + "description": "تحديد كَمّيَّة الرسائل التي سيتم معاينتها" + }, + "prevent_message_sending": { + "name": "منع إرسال الرسالة", + "description": "يمنع إرسال أنواع معينة من الرسائل" + }, + "better_notifications": { + "name": "إشعارات أفضل", + "description": "يضيف المزيد من المعلومات في الإشعارات المستلمة" + }, + "notification_blacklist": { + "name": "الإشعارات المحظورة", + "description": "حدد الإشعارات التي يجب حظرها" + }, + "message_logger": { + "name": "تسجيل الرسائل", + "description": "يمنع حذف الرسائل" + }, + "auto_save_messages_in_conversations": { + "name": "حفظ الرسائل تلقائيًا", + "description": "يحفظ تلقائيًا كل رسالة في المحادثات" + }, + "gallery_media_send_override": { + "name": "تجاوز إرسال الوسائط المرسلة من المعرض", + "description": "ينتحل مصدر الوسائط عند الإرسال من المعرض" + } + } + }, + "global": { + "name": "عالمي", + "description": "ضبط إعدادات Snapchat العالمية", + "properties": { + "spoofLocation": { + "name": "الموقع", + "description": "تزييف الموقع الخاص بك", + "properties": { + "coordinates": { + "name": "الإحداثيات", + "description": "تعيين الإحداثيات" + } + } + }, + "snapchat_plus": { + "name": "سناب شات+", + "description": "تمكين ميزات Snapchat Plus\nقد لا تعمل بعض الميزات على جانب الخادم" + }, + "auto_updater": { + "name": "التحديث التلقائي", + "description": "يتحقق تلقائيًا من توفر تحديثات جديدة" + }, + "disable_metrics": { + "name": "تعطيل المقاييس", + "description": "يحظر إرسال بيانات تحليلية محددة إلى Snapchat" + }, + "block_ads": { + "name": "حجب الإعلانات", + "description": "يمنع عرض الإعلانات" + }, + "bypass_video_length_restriction": { + "name": "تجاوز قيود طول الفيديو", + "description": "فردي: يرسل فيديو واحد\nتقسيم: تقسيم مقاطع الفيديو بعد التحرير" + }, + "disable_google_play_dialogs": { + "name": "تعطيل مربعات حوار خدمات Google Play", + "description": "يمنع ظهور مربعات حوار توفر خدمات Google Play" + }, + "disable_snap_splitting": { + "name": "تعطيل تقسيم السنابة", + "description": "يمنع تقسيم السنابات إلى أجزاء متعددة\nالصور التي ترسلها سوف تتحول إلى مقاطع فيديو" + } + } + }, + "rules": { + "name": "القواعد", + "description": "إدارة الميزات التلقائية للأفراد" + }, + "camera": { + "name": "الكاميرا", + "description": "ضبط الإعدادات الصحيحة للحصول على السنابة المثالية", + "properties": { + "disable_camera": { + "name": "تعطيل الكاميرا", + "description": "يمنع Snapchat من استخدام الكاميرات المتوفرة على جهازك" + }, + "immersive_camera_preview": { + "name": "معاينة غامرة", + "description": "يمنع Snapchat من قص معاينة الكاميرا\nقد يتسبب هذا في وميض الكاميرا على بعض الأجهزة" + }, + "override_preview_resolution": { + "name": "تجاوز دقة العرض", + "description": "يتجاوز دقة عرض الكاميرا" + }, + "override_picture_resolution": { + "name": "تجاوز دقة الصورة", + "description": "يتجاوز دِقَّة الصورة" + }, + "custom_frame_rate": { + "name": "معدل الإطار المخصص", + "description": "يتجاوز معدل إطار الكاميرا" + }, + "force_camera_source_encoding": { + "name": "فرض ترميز مصدر الكاميرا", + "description": "يفرض ترميز مصدر الكاميرا" + } + } + }, + "streaks_reminder": { + "name": "تذكير Streaks", + "description": "يتم إعلامك دورياً عن البث الخاص بك", + "properties": { + "interval": { + "name": "الفترة", + "description": "الفاصل الزمني بين كل تذكير (ساعات)" + }, + "remaining_hours": { + "name": "الوقت المتبقي", + "description": "المدة المتبقية من الوقت قبل عرض الإشعار" + }, + "group_notifications": { + "name": "إشعارات المجموعة", + "description": "تجميع الإشعارات في واحد" + } + } + }, + "experimental": { + "name": "تجريبي", + "description": "ميّزات اختبارية", + "properties": { + "native_hooks": { + "name": "الخطافات الأصلية", + "description": "الميزات غير الآمنة التي ترتبط بالكود الأصلي لـ Snapchat", + "properties": { + "disable_bitmoji": { + "name": "تعطيل Bitmoji", + "description": "تعطيل ملف تعريف الأصدقاء Bitmoji" + } + } + }, + "spoof": { + "name": "محاكاة", + "description": "محاكاة لمعلومات مختلفة عنك" + }, + "app_passcode": { + "name": "رمز مرور التطبيق", + "description": "يعيّن رمز مرور لقفل التطبيق" + }, + "app_lock_on_resume": { + "name": "قفل التطبيق عند الإستئناف", + "description": "يقفل التطبيق عند إعادة فتحه" + }, + "infinite_story_boost": { + "name": "تعزيز القصّة بلا حدود", + "description": "تجاوز حد تأخير تعزيز القصة" + }, + "meo_passcode_bypass": { + "name": "تجاوز رمز مرور خاصية خاص بي فقط", + "description": "تجاوز رمز مرور خاصية خاص بي فقط\nلن يعمل هذا إلا إذا تم إدخال رمز المرور بشكل صحيح من قبل" + }, + "unlimited_multi_snap": { + "name": "سناب متعدد غير محدود", + "description": "يتيح لك التقاط كمية غير محدودة من السنابات المتعددة" + }, + "no_friend_score_delay": { + "name": "إزالة التأخير في تحديث نقاط الأصدقاء", + "description": "يزيل التأخير عند عرض نقاط الأصدقاء" + }, + "e2ee": { + "name": "التشفير من النهاية إلى النهاية", + "description": "يقوم بتشفير رسائلك باستخدام AES باستخدام مفتاح سري مشترك\nتأكد من حفظ مفتاحك في مكان آمن!", + "properties": { + "encrypted_message_indicator": { + "name": "مؤشر الرسالة المشفرة", + "description": "يضيف 🔒 emoji بجوار الرسائل المشفرة" + }, + "force_message_encryption": { + "name": "فرض تشفير الرسالة", + "description": "يمنع إرسال رسائل مشفرة إلى الأشخاص الذين لم يتم تمكين تشفير E2E لديهم إلا عند تحديد محادثات متعددة" + } + } + }, + "add_friend_source_spoof": { + "name": "انتحال مصدر إضافة صديق", + "description": "ينتحل مصدر طلب الصداقة" + }, + "hidden_snapchat_plus_features": { + "name": "مميزات Snapchat Plus المخفية", + "description": "لتمكين ميزات Snapchat Plus التي لم يتم إصدارها/التجريبية\nقد لا يعمل على إصدارات Snapchat الأقدم" + } + } + }, + "scripting": { + "name": "البرمجيات", + "description": "تشغيل البرامج النصية المخصصة لتوسيع SnapEnhance", + "properties": { + "developer_mode": { + "name": "وضع المطور", + "description": "عرض معلومات التصحيح على واجهة المستخدم Snapchat" + }, + "module_folder": { + "name": "مجلد الوحدة", + "description": "المجلد الذي توجد فيه البرامج النصية" + } + } + } }, - "android_id": { - "name": "Android ID", - "description": "Spoofs the devices Android ID" - } - }, - "option": { - "property": { + "options": { + "app_appearance": { + "always_light": "مضيءٌ دائمًا", + "always_dark": "مظلمٌ دائِمًا" + }, "better_notifications": { - "chat": "عرض رسائل الدردشة", - "snap": "عرض الوسائط", "reply_button": "إضافة زر الرد", - "download_button": "إضافة زر التنزيل" + "download_button": "إضافة زر التنزيل", + "group": "إشعارات المجموعة" }, "friend_feed_menu_buttons": { - "auto_download_blacklist": "⬇️ قائمة التنزيل التلقائي المحظورة", - "anti_auto_save": "💬 منع حفظ الرسائل تلقائياً", - "stealth_mode": "👻 وضع التخفي", - "conversation_info": "👤 معلومات المحادثة" + "auto_download": "⬇️ تنزيل تلقائي", + "auto_save": "💬 حفظ الرسائل تلقائيًا", + "stealth": "👻 وضع التخفي", + "conversation_info": "👤 معلومات المحادثة", + "e2e_encryption": "🔒 استخدام تشفير E2E" }, - "download_options": { - "allow_duplicate": "السماح بالتنزيلات المكررة", - "create_user_folder": "إنشاء مجلد لكل مستخدم", + "path_format": { + "create_author_folder": "إنشاء مجلد لكل مؤلف", + "create_source_folder": "إنشاء مجلد لكل نوع من مصادر الوسائط", "append_hash": "إضافة هاش فريد إلى اسم الملَفّ", + "append_source": "إضافة مصدر الوسائط إلى اسم الملَفّ", "append_username": "إضافة اسم المستخدم إلى اسم الملَفّ", - "append_date_time": "إضافة التاريخ والوقت إلى اسم الملَفّ", - "append_type": "إضافة نوع الوسائط إلى اسم الملَفّ", - "merge_overlay": "دمج تداخل النص مع السنابة" + "append_date_time": "إضافة التاريخ والوقت إلى اسم الملَفّ" }, - "auto_download_options": { + "auto_download_sources": { "friend_snaps": "سنابات الأصدقاء", "friend_stories": "قصص الأصدقاء", "public_stories": "القصص العامة", "spotlight": "منصة الأضواء" }, - "download_logging": { + "logging": { "started": "بدأ", "success": "نجح", "progress": "مستوى التقدُّم", "failure": "فشل" }, - "auto_save_messages": { - "NOTE": "الملاحظات الصوتية", - "CHAT": "الدردشة", - "EXTERNAL_MEDIA": "الوسائط الخارجية", - "SNAP": "السنابة", - "STICKER": "الملصقات" - }, "notifications": { "chat_screenshot": "لقطة الشاشة", "chat_screen_record": "تسجيل الشاشة", + "snap_replay": "إعادة عرض السنابة", "camera_roll_save": "حُفظ بالمعرض", "chat": "الدردشة", "chat_reply": "الرد على الدردشة", "snap": "السنابة", "typing": "جارٍ الكتابة", "stories": "القصص", + "chat_reaction": "رد فعل DM", + "group_chat_reaction": "رد فعل المجموعة", "initiate_audio": "مكالمة صوتية واردة", "abandon_audio": "مكالمة صوتية فائتة", "initiate_video": "مكالمة فيديو واردة", @@ -289,41 +569,38 @@ "ORIGINAL": "الأصل", "NOTE": "الملاحظات الصوتية", "SNAP": "السنابة", - "LIVE_SNAP": "السنابة مع الصوت" - }, - "hide_ui_elements": { - "remove_call_buttons": "إزالة أزرار المكالمات", - "remove_cognac_button": "إزالة زر Cognac", - "remove_live_location_share_button": "إزالة زر مشاركة الموقع المباشر", - "remove_stickers_button": "إزالة زر الملصقات", - "remove_voice_record_button": "إزالة زر تسجيل الصوت", - "remove_camera_borders": "إزالة حدود الكاميرا" - }, - "auto_updater": { - "DISABLED": "معطّل", - "EVERY_LAUNCH": "كل عملية تشغيل", - "DAILY": "يوميًا", - "WEEKLY": "إسبوعيٍا" + "SAVABLE_SNAP": "سنابة قابلة للحفظ" }, - "story_viewer_override": { - "OFF": "إيقاف", - "DISCOVER_PLAYBACK_SEEKBAR": "تمكين اكتشاف شريط التقدم عن التشغيل", - "VERTICAL_STORY_VIEWER": "تمكين عارض شريط تقدم عداد القصة بأعلى السنابات" + "hide_ui_components": { + "hide_profile_call_buttons": "إزالة أزرار الاتصال بالملف الشخصي", + "hide_chat_call_buttons": "إزالة أزرار الاتصال بالدردشة", + "hide_live_location_share_button": "إزالة زر مشاركة الموقع المباشر", + "hide_stickers_button": "إزالة زر الملصقات", + "hide_voice_record_button": "إزالة زر تسجيل الصوت" }, - "hide_story_section": { - "hide_friend_suggestions": "إخفاء اقتراحات الأصدقاء", + "hide_story_sections": { + "hide_friend_suggestions": "إخفاء اقتراحات صديق", "hide_friends": "إخفاء قسم الأصدقاء", - "hide_following": "إخفاء قسم المتابعة", + "hide_suggested": "إخفاء قسم المقترح", "hide_for_you": "إخفاء قسم اكتشف" }, - "startup_page_override": { - "OFF": "إيقاف", - "ngs_map_icon_container": "الخريطة", - "ngs_chat_icon_container": "الدردشة", - "ngs_camera_icon_container": "الكاميرا", - "ngs_community_icon_container": "المجتمع / القصص", - "ngs_spotlight_icon_container": "منصة الأضواء", - "ngs_search_icon_container": "البحث" + "home_tab": { + "map": "الخريطة", + "chat": "الدردشة", + "camera": "الكاميرا", + "discover": "إكتشف", + "spotlight": "منصة الأضواء" + }, + "add_friend_source_spoof": { + "added_by_username": "حسب اسم المستخدم", + "added_by_mention": "حسب الاشارة", + "added_by_group_chat": "حسب دردشة المجموعة", + "added_by_qr_code": "حسب رمز QR", + "added_by_community": "حسب المجتمع" + }, + "bypass_video_length_restriction": { + "single": "وسائط منفردة", + "split": "تقسيم الوسائط" } } }, @@ -333,10 +610,6 @@ "auto_download_blacklist": "قائمة التنزيل التلقائي المحظورة", "anti_auto_save": "منع الحفظ التلقائي" }, - "message_context_menu_option": { - "download": "تنزيل", - "preview": "معاينة" - }, "chat_action_menu": { "preview_button": "معاينة", "download_button": "تنزيل", @@ -360,26 +633,21 @@ }, "profile_info": { "title": "معلومات الملَفّ الشخصي", - "username": "اسم المستخدم", + "first_created_username": "أول اسم مستخدم تم إنشاؤه", + "mutable_username": "اسم مستخدم قابل للتغيير", "display_name": "اسم العرض", "added_date": "تاريخ الإضافة", - "birthday": "تاريخ الميلاد: {month} {day}" - }, - "auto_updater": { - "no_update_available": "لا يوجد تحديث متوفر!", - "dialog_title": "تحديث جديد متوفر!", - "dialog_message": "يتوفر تحديث جديد لـ SnapEnhance! ({version})\n\n{body}", - "dialog_positive_button": "تنزيل و تثبيت", - "dialog_negative_button": "إلغاء", - "downloading_toast": "جارٍ تنزيل التحديث...", - "download_manager_notification_title": "جارٍ تنزيل SnapEnhance APK..." + "birthday": "تاريخ الميلاد: {month} {day}", + "friendship": "الصداقة", + "add_source": "إضافة مصدر", + "snapchat_plus": "سناب شات+", + "snapchat_plus_state": { + "subscribed": "مشترِك", + "not_subscribed": "غير مشترِك" + } }, "chat_export": { - "select_export_format": "تحديد تنسيق التصدير", - "select_media_type": "تحديد أنواع الوسائط للتصدير", - "select_conversation": "حدد محادثة للتصدير", "dialog_negative_button": "إلغاء", - "dialog_neutral_button": "تصدير الكل", "dialog_positive_button": "تصدير", "exported_to": "تم التصدير إلى {path}", "exporting_chats": "تصدير الدردشة...", @@ -395,37 +663,28 @@ "positive": "نعم", "negative": "لا", "cancel": "إلغاء", - "open": "فتح" + "open": "فتح", + "download": "تنزيل" }, - "download_manager_activity": { - "remove_all_title": "إزالة جميع التنزيلات", - "remove_all_text": "هل أنت متأكد من أنك تريد القيام بهذا؟", - "remove_all": "إزالة الكل", - "no_downloads": "لا توجد تنزيلات", - "cancel": "إلغاء", - "file_not_found_toast": "الملَفّ غير موجود!", - "category": { - "all_category": "الكل", - "pending_category": "معلّقة", - "snap_category": "السنابات", - "story_category": "القصص", - "spotlight_category": "منصة الأضواء" - }, - "debug_settings": "إعدادات التصحيح", - "debug_settings_page": { - "clear_file_title": "مسح ملَفّ {file_name}", - "clear_file_confirmation": "هل أنت متأكد من أنك تريد مسح الملَفّ {file_name}؟", - "clear_cache_title": "تنظيف ذاكرة التخزين المؤقت", - "reset_all_title": "إعادة تعيين كل الإعدادات", - "reset_all_confirmation": "هل أنت متأكد من أنك تريد إعادة تعيين كل الإعدادات؟", - "success_toast": "نجح!", - "device_spoofer": "خداع الجهاز" - } + "profile_picture_downloader": { + "button": "تنزيل صورة الملف الشخصي", + "title": "تم تنزيل صورة الملف الشخصي", + "avatar_option": "الصورة الرمزيّة", + "background_option": "الخلفية" }, "download_processor": { + "attachment_type": { + "snap": "السنابة", + "sticker": "الملصقات", + "external_media": "الوسائط الخارجية", + "note": "الملاحظة", + "original_story": "القصة الأصلية" + }, + "select_attachments_title": "حدد المرفقات للتنزيل", "download_started_toast": "بدأ التنزيل", "unsupported_content_type_toast": "نوع المحتوى غير مدعوم!", "failed_no_longer_available_toast": "الوسائط لم تعد متوفرة", + "no_attachments_toast": "لم يتم العثور على مرفقات!", "already_queued_toast": "الوسائط موجودة بالفعل في قائمة الانتظار!", "already_downloaded_toast": "تم تنزيل الوسائط بالفعل!", "saved_toast": "تم الحفظ في {path}", @@ -433,15 +692,11 @@ "processing_toast": "جارٍ معالجة {path}...", "failed_generic_toast": "فشل التنزيل", "failed_to_create_preview_toast": "فشل إنشاء المعاينة", - "failed_processing_toast": "فشل في معالجة {error}", - "failed_gallery_toast": "فشل الحفظ في معرض الصور {error}" - }, - "config_activity": { - "title": "إعدادات SnapEnhance", - "selected_text": "{count} المحدد", - "invalid_number_toast": "رقم غير صالح!" + "failed_processing_toast": "فشلت المعالجة {error}", + "failed_gallery_toast": "فشل الحفظ في المعرض {error}" }, - "spoof_activity": { - "title": "خداع الإعدادات" + "streaks_reminder": { + "notification_title": "Streaks", + "notification_text": "ستفقد Streak مع {friend} خلال {hoursLeft} ساعة" } -} \ No newline at end of file +} diff --git a/common/src/main/assets/lang/bn.json b/common/src/main/assets/lang/bn.json new file mode 100644 index 000000000..5d84fe936 --- /dev/null +++ b/common/src/main/assets/lang/bn.json @@ -0,0 +1,42 @@ +{ + "conversation_preview": { + "unknown_user": "অজানা ব্যবহাকারী" + }, + "profile_info": { + "title": "প্রোফাইল তথ্য", + "display_name": "প্রদর্শন নাম", + "added_date": "যুক্ত হওয়ার তারিখ", + "birthday": "জন্মদিন : {month}{day}" + }, + "chat_export": { + "dialog_negative_button": "বাতিল করুন", + "dialog_positive_button": "রপ্তানি করুন", + "exported_to": "রপ্তানি করা হয়েছে {path}", + "exporting_chats": "চ্যাট রপ্তানি করা হচ্ছে...", + "processing_chats": "{amount} গুলো আলাপ প্রসেসিং করা হচ্ছে...", + "export_fail": "{conversation} আলাপ রপ্তানি করতে সক্ষম হয়নি", + "writing_output": "আউটপুট লিখা হচ্ছে...", + "finished": "সম্পন্ন! আপনি এখন ডায়ালগটি বন্ধ করে দিতে পারেন।.", + "no_messages_found": "কোন বার্তা পাওয়া যায়নি!", + "exporting_message": "{conversation} রপ্তানি করা হচ্ছে..." + }, + "button": { + "ok": "ঠিক আছে", + "positive": "হ্যাঁ", + "negative": "না", + "cancel": "বাতিল করুন", + "open": "খুলুন" + }, + "download_processor": { + "download_started_toast": "ডাউনলোড শুরু হয়েছে", + "unsupported_content_type_toast": "অসমর্থিত কনটেন্ট ধরণ!", + "failed_no_longer_available_toast": "মিডিয়া আর পাওয়া যাচ্ছে না", + "already_queued_toast": "মিডিয়া ইতিমধ্যে কিউতে আছে!", + "already_downloaded_toast": "মিডিয়া ইতোমধ্যে ডাউনলোড হয়েছে!", + "saved_toast": "সংরক্ষিত হয়েছে {path}", + "download_toast": "ডাউনলোড হচ্ছে {path}...", + "processing_toast": "প্রসেসিং হচ্ছে {path}...", + "failed_generic_toast": "ডাউনলোড করতে সক্ষম হয়নি", + "failed_to_create_preview_toast": "পুর্বরূপ তৈরি করতে সক্ষম হয়নি" + } +} diff --git a/common/src/main/assets/lang/da.json b/common/src/main/assets/lang/da.json new file mode 100644 index 000000000..11a65ba0d --- /dev/null +++ b/common/src/main/assets/lang/da.json @@ -0,0 +1,699 @@ +{ + "setup": { + "dialogs": { + "select_language": "Vælg sprog", + "save_folder": "SnapEnhance kræver Lagringstilladelser til at downloade og gemme medier fra Snapchat.\nVælg venligst det sted, hvor medierne skal downloades til.", + "select_save_folder_button": "Vælg Mappe" + }, + "mappings": { + "dialog": "For dynamisk at støtte en bred vifte af Snapchat versioner, tilknytninger er nødvendige for SnapEnhance at fungere ordentligt, bør dette ikke tage mere end 5 sekunder.", + "generate_button": "Generer", + "generate_failure_no_snapchat": "SnapEnhance kunne ikke detektere Snapchat, prøv at geninstallere Snapchat.", + "generate_failure": "Der opstod en fejl under forsøget på at generere mappings, prøv venligst igen.", + "generate_success": "Tilpasninger genereret med succes." + }, + "permissions": { + "dialog": "For at fortsætte skal du passe til følgende krav:", + "notification_access": "Adgang til notifikationer", + "battery_optimization": "Batterioptimering", + "display_over_other_apps": "Vis over andre apps", + "request_button": "Forespørgsel" + } + }, + "manager": { + "routes": { + "features": "Funktioner", + "home": "Hjem", + "home_settings": "Indstillinger", + "home_logs": "Logfiler", + "social": "Social", + "scripts": "Scripts" + }, + "sections": { + "home": { + "logs": { + "clear_logs_button": "Tøm log", + "export_logs_button": "Eksporter logs" + } + }, + "features": { + "disabled": "Slået fra" + }, + "social": { + "e2ee_title": "Ende-til-Ende kryptering", + "rules_title": "Regler", + "participants_text": "%{antal} deltagere", + "not_found": "Ikke fundet", + "streaks_title": "Streaks", + "streaks_length_text": "Længde: {length}", + "streaks_expiration_short": "{hours} timer", + "streaks_expiration_text": "Udløber om {eta}", + "reminder_button": "Sæt påmindelse" + } + }, + "dialogs": { + "add_friend": { + "title": "Tilføj ven eller gruppe", + "search_hint": "Søg", + "fetch_error": "Dataene blev ikke hentet", + "category_groups": "Grupper", + "category_friends": "Venner" + } + } + }, + "rules": { + "modes": { + "blacklist": "Sortlistetilstand", + "whitelist": "Whitelist tilstand" + }, + "properties": { + "auto_download": { + "name": "Auto-download", + "description": "Download automatisk snaps, når du ser dem", + "options": { + "blacklist": "Udeluk fra automatisk download", + "whitelist": "Auto-hentning" + } + }, + "stealth": { + "name": "Stealth Tilstand", + "description": "Forhindrer alle i at vide, at du har åbnet deres Snaps/Chats og samtaler", + "options": { + "blacklist": "Udeluk fra Stealth Mode", + "whitelist": "Stealth Tilstand" + } + }, + "auto_save": { + "name": "Gem Automatisk", + "description": "Gemmer chatbeskeder når de vises", + "options": { + "blacklist": "Udeluk fra automatisk lagring", + "whitelist": "Auto-gem" + } + }, + "hide_friend_feed": { + "name": "Skjul fra venskabsfeed" + }, + "e2e_encryption": { + "name": "Brug E2E Kryptering" + }, + "pin_conversation": { + "name": "Fastgør samtale" + } + } + }, + "actions": { + "clean_snapchat_cache": "Ryd Snapchat-Cache", + "clear_message_logger": "Ryd Beskedlogger", + "refresh_mappings": "Opdater Tilføjelser", + "open_map": "Vælg placering på kort", + "check_for_updates": "Tjek for opdateringer", + "export_chat_messages": "Eksportér Chatbeskeder" + }, + "features": { + "notices": { + "unstable": "⚠️ Ustabil", + "ban_risk": "⚠️ Denne funktion kan medføre forbud", + "internal_behavior": "⚠️ Dette kan ødelægge Snapchat intern opførsel", + "require_native_hooks": "⚠️ Denne funktion kræver eksperimentelle indfødte kroge til at fungere korrekt" + }, + "properties": { + "downloader": { + "name": "Henter", + "description": "Download Snapchat Medie", + "properties": { + "save_folder": { + "name": "Gem Mappe", + "description": "Vælg den mappe som alle medier skal downloades til" + }, + "auto_download_sources": { + "name": "Download Kilder Automatisk", + "description": "Vælg de kilder, der skal downloades automatisk fra" + }, + "prevent_self_auto_download": { + "name": "Forhindr Automatisk Download", + "description": "Forhindrer dine egne Snaps i automatisk at blive hentet" + }, + "path_format": { + "name": "Sti Format", + "description": "Angiv filstiens format" + }, + "allow_duplicate": { + "name": "Tillad Dupliker", + "description": "Tillader, at de samme medier downloades flere gange" + }, + "merge_overlays": { + "name": "Flet Overlejringer", + "description": "Kombinerer teksten og medierne for en Snap i en enkelt fil" + }, + "force_image_format": { + "name": "Gennemtving Billedformat", + "description": "Tving billeder til at blive gemt i et angivet format" + }, + "force_voice_note_format": { + "name": "Gennemtving Stemme Note Format", + "description": "Tving billeder til at blive gemt i et angivet format" + }, + "download_profile_pictures": { + "name": "Download Profilbilleder", + "description": "Tillader dig at downloade profilbilleder fra profilsiden" + }, + "chat_download_context_menu": { + "name": "Chat Download Kontekstmenu", + "description": "Tillader dig at downloade medier fra en samtale ved at trykke længe på dem" + }, + "ffmpeg_options": { + "name": "FFmpeg-indstillinger", + "description": "Angiv yderligere FFmpeg indstillinger", + "properties": { + "threads": { + "name": "Tråde", + "description": "Mængden af tråde der skal bruges" + }, + "preset": { + "name": "Forvalg", + "description": "Indstil hastigheden for konverteringen" + }, + "constant_rate_factor": { + "name": "Konstant Rate Faktor", + "description": "Indstil den konstante hastighedsfaktor for video-encoder\nFra 0 til 51 for libx264" + }, + "video_bitrate": { + "name": "Videobit-hastighed", + "description": "Indstil videoens bitrate (kbps)" + }, + "audio_bitrate": { + "name": "Audiobit-hastighed", + "description": "Indstil videoens bitrate (kbps)" + }, + "custom_video_codec": { + "name": "Brugerdefineret Lydkode", + "description": "Angiv en brugerdefineret Video Codec (f.eks. libx264)" + }, + "custom_audio_codec": { + "name": "Brugerdefineret Lydkode", + "description": "Angiv en brugerdefineret Video Codec (f.eks. libx264)" + } + } + }, + "logging": { + "name": "Logging", + "description": "Viser toasts, når mediet downloades" + } + } + }, + "user_interface": { + "name": "Brugergrænseflade", + "description": "Skift udseendet og fornemmelsen af Snapchat", + "properties": { + "enable_app_appearance": { + "name": "Aktiver App-udseende Indstillinger", + "description": "Aktiverer den skjulte App Udseende Indstilling\nKan ikke kræves i nyere Snapchat versioner" + }, + "amoled_dark_mode": { + "name": "AMOLED Mørk Tilstand", + "description": "Aktiverer AMOLED mørk tilstand\nSørg for at Snapchats Mørk tilstand er aktiveret" + }, + "friend_feed_message_preview": { + "name": "Forhåndsvisning Af Venne Feed Besked", + "description": "Viser en forhåndsvisning af de sidste beskeder i venskabsfeedet", + "properties": { + "amount": { + "name": "Mængde", + "description": "Antallet af beskeder der skal forhåndsvises" + } + } + }, + "bootstrap_override": { + "name": "Bootstrap Overskriv", + "description": "Tilsidesætter indstillinger for brugergrænseflade bootstrap", + "properties": { + "app_appearance": { + "name": "App Udseende", + "description": "Indstiller en vedvarende app-udseende" + }, + "home_tab": { + "name": "Fanebladet Hjem", + "description": "Tilsidesætter fanen opstart, når du åbner Snapchat" + } + } + }, + "map_friend_nametags": { + "name": "Forbedrede Vennekortnavne", + "description": "Forbedrer Nametags af venner på Snapmap" + }, + "streak_expiration_info": { + "name": "Vis Streak Udløbsinfo", + "description": "Viser en Streak- udløbstimer ved siden af Streaks tælleren" + }, + "hide_friend_feed_entry": { + "name": "Skjul Venne Feed Post", + "description": "Skjuler en bestemt ven fra venskabsfeed\nBrug fanen social til at håndtere denne funktion" + }, + "hide_streak_restore": { + "description": "Skjuler knappen Gendan i vennefeedet" + }, + "hide_story_sections": { + "name": "Skjul Afsnittet Historie", + "description": "Skjul visse brugergrænsefladeelementer, der vises i afsnittet historie" + }, + "hide_ui_components": { + "name": "Skjul UI Komponenter", + "description": "Vælg hvilke brugergrænsefladekomponenter der skal skjules" + }, + "disable_spotlight": { + "name": "Deaktivér Spotlight", + "description": "Deaktiverer Spotlight siden" + }, + "friend_feed_menu_buttons": { + "name": "Ven Feed Menu-Knapper", + "description": "Vælg hvilke knapper der skal vises i menulinjen Venne Feed" + }, + "friend_feed_menu_position": { + "name": "Ven Feed Position Indeks", + "description": "Placeringen af Venne Feed Menu komponent" + }, + "enable_friend_feed_menu_bar": { + "name": "Ven Feed Menu-Knapper", + "description": "Aktiverer den nye vennefeed menulinjen" + } + } + }, + "messaging": { + "name": "Meddelelser", + "description": "Skift hvordan du interagerer med venner", + "properties": { + "anonymous_story_viewing": { + "name": "Anonym Historievisning", + "description": "Forhindrer alle i at kende du har set deres historie" + }, + "hide_bitmoji_presence": { + "name": "Skjul Bitmoji Tilstedeværelse", + "description": "Forhindrer din Bitmoji i at dukke op, mens du er i Chat" + }, + "hide_typing_notifications": { + "name": "Skjul Skrivenotifikationer", + "description": "Forhindrer alle i at vide, at du skriver en besked" + }, + "unlimited_snap_view_time": { + "name": "Ubegrænset Snap Visningstid", + "description": "Fjerner tidsgrænsen for visning af Snaps" + }, + "disable_replay_in_ff": { + "name": "Deaktivér genafspilning i FF", + "description": "Deaktiverer evnen til at genspille med et langt tryk fra Vennefeed" + }, + "message_preview_length": { + "name": "Besked Forhåndsvisning Længde", + "description": "Antallet af beskeder der skal forhåndsvises" + }, + "prevent_message_sending": { + "name": "Forhindre Besked Afsendelse", + "description": "Forhindrer afsendelse af visse typer beskeder" + }, + "better_notifications": { + "name": "Bedre Notifikationer", + "description": "Tilføjer mere information i modtagne notifikationer" + }, + "notification_blacklist": { + "name": "Notifikation Sortliste", + "description": "Vælg notifikationer som skal blive blokeret" + }, + "message_logger": { + "name": "Besked Logger", + "description": "Forhindrer beskeder i at blive slettet" + }, + "auto_save_messages_in_conversations": { + "name": "Gem Automatisk Beskeder", + "description": "Gem automatisk alle beskeder i samtaler" + }, + "gallery_media_send_override": { + "name": "Galleri Medier Send Overskriv", + "description": "Spoofs mediekilden, når du sender fra Galleri" + } + } + }, + "global": { + "name": "Global", + "description": "Tweak Globale Snapchat-Indstillinger", + "properties": { + "spoofLocation": { + "name": "Placering", + "description": "Spoof din placering", + "properties": { + "coordinates": { + "name": "Koordinater", + "description": "Sæt koordinaterne" + } + } + }, + "snapchat_plus": { + "name": "Snapchat Plus", + "description": "Aktiverer Snapchat Plus-funktioner\nNogle server-sidede funktioner fungerer muligvis ikke" + }, + "auto_updater": { + "name": "Automatisk Opdatering", + "description": "Søg automatisk efter opdateringer" + }, + "disable_metrics": { + "name": "Deaktivér Metrics", + "description": "Blokerer afsendelse af specifikke analytiske data til Snapchat" + }, + "block_ads": { + "name": "Bloker reklamer", + "description": "Forhindrer reklamer i at blive vist" + }, + "bypass_video_length_restriction": { + "name": "Bypass Videolængde Begrænsninger", + "description": "Single: sender en enkelt video\nSplit: split videoer efter redigering" + }, + "disable_google_play_dialogs": { + "name": "Deaktiver Google Play-tjenester Dialoger", + "description": "Forhindre, at Google Play-tjenesternes tilgængelighedsdialoger vises" + }, + "disable_snap_splitting": { + "name": "Deaktivér Snap Opdeling", + "description": "Forhindrer snaps i at blive opdelt i flere dele\nBilleder, du sender, vil blive til videoer" + } + } + }, + "rules": { + "name": "Regler", + "description": "Administrer automatiske funktioner for individuelle personer" + }, + "camera": { + "name": "Kamera", + "description": "Juster de rigtige indstillinger for den perfekte snap", + "properties": { + "disable_camera": { + "name": "Deaktivér Kamera", + "description": "Forhindrer Snapchat i at bruge de kameraer, der er tilgængelige på din enhed" + }, + "immersive_camera_preview": { + "name": "Omfattende Forhåndsvisning", + "description": "Forhindrer Snapchat i at beskære kamera forhåndsvisning\nDette kan få kameraet til at flimre på nogle enheder" + }, + "override_preview_resolution": { + "name": "Tilsidesæt Forhåndsvisning Opløsning", + "description": "Tilsidesætter kameraets forhåndsvisningsopløsning" + }, + "override_picture_resolution": { + "name": "Tilsidesæt Forhåndsvisning Opløsning", + "description": "Tilsidesæt Forhåndsvisning Opløsning" + }, + "custom_frame_rate": { + "name": "Tilpasset Rammehastighed", + "description": "Tilsidesætter kameraets billedfrekvens" + }, + "force_camera_source_encoding": { + "name": "Tving Kameraets Kildekodning", + "description": "Tving Kameraets Kildekodning" + } + } + }, + "streaks_reminder": { + "name": "Streaks Påmindelse", + "description": "Periodisk giver dig besked om dine streaks", + "properties": { + "interval": { + "description": "Intervallet mellem hver påmindelse (timer)" + }, + "remaining_hours": { + "name": "Resterende tid", + "description": "Det resterende tidsrum før meddelelsen vises" + }, + "group_notifications": { + "name": "Gruppe Notifikationer", + "description": "Grupper notifikationer til en enkelt" + } + } + }, + "experimental": { + "name": "Eksperimental", + "description": "Eksperimentelle funktioner", + "properties": { + "native_hooks": { + "name": "Native Kroge", + "description": "Usikre funktioner, der hookes ind i Snapchat's native kode", + "properties": { + "disable_bitmoji": { + "name": "Deaktivér Bitmoji", + "description": "Deaktiverer Venner Profil Bitmoji" + } + } + }, + "spoof": { + "name": "Spoof", + "description": "Spoof forskellige oplysninger om dig" + }, + "app_passcode": { + "name": "App Pin-kode", + "description": "Sætter en adgangskode til at låse app'en" + }, + "app_lock_on_resume": { + "name": "App Lås Ved Genoptagelse", + "description": "Låser appen, når den genåbnes" + }, + "infinite_story_boost": { + "name": "Uendelig Historie Boost", + "description": "Bypass Story Boost Limit delay" + }, + "meo_passcode_bypass": { + "name": "Mine Øjne Kun Kode Bypass", + "description": "Bypass My Eyes Only Passcode\nDette vil kun fungere, hvis adgangskoden er indtastet korrekt før" + }, + "unlimited_multi_snap": { + "name": "Ubegrænset Multi Snap", + "description": "Tillader dig at tage en ubegrænset mængde Multi Snaps" + }, + "no_friend_score_delay": { + "name": "Ingen Vennescore Forsinkelse", + "description": "Fjerner forsinkelsen, når du ser en Venners Score" + }, + "e2ee": { + "name": "Ende-til-Ende kryptering", + "description": "Krypterer dine beskeder med AES ved hjælp af en delt hemmelig nøgle\nSørg for at gemme din nøgle et sikkert sted!", + "properties": { + "encrypted_message_indicator": { + "name": "Krypteret Meddelelsesindikator", + "description": "Tilføjer en 🔒 emoji ved siden af krypterede beskeder" + }, + "force_message_encryption": { + "name": "Tving besked Kryptering", + "description": "Forhindrer afsendelse af krypterede beskeder til personer, der ikke har E2E Kryptering aktiveret kun, når flere samtaler er valgt" + } + } + }, + "add_friend_source_spoof": { + "name": "Tilføj Ven Kilde Spoof", + "description": "Spoofs kilden til en venneanmodning" + }, + "hidden_snapchat_plus_features": { + "name": "Skjult Snapchat Plus-funktioner", + "description": "Aktiverer ikke-frigivet/beta Snapchat Plus funktioner\nKan ikke virke på ældre snapchat versioner" + } + } + }, + "scripting": { + "name": "Scripting", + "description": "Kør brugerdefinerede scripts for at udvide SnapEnhance", + "properties": { + "developer_mode": { + "name": "Udviklertilstand", + "description": "Viser debug info på Snapchats brugerflade" + }, + "module_folder": { + "name": "Modul Mappe", + "description": "Mappen hvor scripterne er placeret" + } + } + } + }, + "options": { + "app_appearance": { + "always_light": "Altid Lys", + "always_dark": "Altid Mørk" + }, + "better_notifications": { + "reply_button": "Tilføj svar-knap", + "download_button": "Tilføj download-knap", + "group": "Gruppenotifikationer" + }, + "friend_feed_menu_buttons": { + "auto_download": "⬇️ Auto-download", + "auto_save": "💬 Gem Automatisk Beskeder", + "stealth": "👻 Stealth Tilstand", + "conversation_info": "👤 Samtaleinfo", + "e2e_encryption": "🔒 Brug E2E Kryptering" + }, + "path_format": { + "create_author_folder": "Opret mappe for hver forfatter", + "create_source_folder": "Opret mappe for hver mediekildetype", + "append_hash": "Tilføj et unikt hash til filnavnet", + "append_source": "Tilføj mediekilden til filnavnet", + "append_username": "Tilføj mediekilden til filnavnet", + "append_date_time": "Tilføj dato og tid til filnavnet" + }, + "auto_download_sources": { + "friend_snaps": "Ven Snaps", + "friend_stories": "Vennehistorier", + "public_stories": "Offentlige Historier", + "spotlight": "Spotlight" + }, + "logging": { + "started": "Startet", + "success": "Succes", + "progress": "Fremskridt", + "failure": "Fejl" + }, + "notifications": { + "chat_screenshot": "Skærmbillede", + "chat_screen_record": "Skærmoptagelse", + "snap_replay": "Fastgør Genafspilning", + "camera_roll_save": "Gem Kamera Rulle", + "chat": "Chat", + "chat_reply": "Chat Svar", + "snap": "Snap", + "typing": "Indtastning", + "stories": "Historier", + "chat_reaction": "DM Reaktion", + "group_chat_reaction": "Gruppe Reaktion", + "initiate_audio": "Indgående Lydopkald", + "abandon_audio": "Ubesvaret Lydopkald", + "initiate_video": "Indgående videoopkald", + "abandon_video": "Ubesvarede videoopkald" + }, + "gallery_media_send_override": { + "ORIGINAL": "Original", + "NOTE": "Lyd Note", + "SNAP": "Snap", + "SAVABLE_SNAP": "Gembar Klip" + }, + "hide_ui_components": { + "hide_profile_call_buttons": "Fjern Profilopkaldsknapper", + "hide_chat_call_buttons": "Fjern Profilopkaldsknapper", + "hide_live_location_share_button": "Fjern Knappen Live Placeringsdeling", + "hide_stickers_button": "Fjern Klistermærker Knap", + "hide_voice_record_button": "Fjern Stemmeoptagelsesknap" + }, + "hide_story_sections": { + "hide_friend_suggestions": "Skjul venneforslag", + "hide_friends": "Skjul vennesektion", + "hide_suggested": "Skjul foreslået sektion", + "hide_for_you": "Skjul sektion for dig" + }, + "home_tab": { + "map": "Kort", + "chat": "Chat", + "camera": "Kamera", + "discover": "Opdag", + "spotlight": "Spotlight" + }, + "add_friend_source_spoof": { + "added_by_username": "Efter Brugernavn", + "added_by_mention": "Efter Omtale", + "added_by_group_chat": "Efter Gruppechat", + "added_by_qr_code": "Via QR-kode", + "added_by_community": "Af Fællesskabet" + }, + "bypass_video_length_restriction": { + "single": "Enkelt medie", + "split": "Opdel medier" + } + } + }, + "friend_menu_option": { + "preview": "Forhåndsvisning", + "stealth_mode": "Stealth Tilstand", + "anti_auto_save": "Anti Automatisk Gem" + }, + "chat_action_menu": { + "preview_button": "Forhåndsvisning", + "download_button": "Hent", + "delete_logged_message_button": "Slet Logget Besked" + }, + "opera_context_menu": { + "download": "Hent medie" + }, + "modal_option": { + "profile_info": "Profiloplysninger", + "close": "Luk" + }, + "gallery_media_send_override": { + "multiple_media_toast": "Du kan kun sende et medie ad gangen" + }, + "conversation_preview": { + "streak_expiration": "udløber på {day} dage {hour} timer {minute} minutter", + "total_messages": "Total sendt/modtagne meddelelser: {count}", + "title": "Forhåndsvisning", + "unknown_user": "Ukendt bruger" + }, + "profile_info": { + "title": "Profiloplysninger", + "first_created_username": "Første Oprettet Brugernavn", + "mutable_username": "Mutabelt Brugernavn", + "display_name": "Visningsnavn", + "added_date": "Tilføjet Dato", + "birthday": "Fødselsdag : {month} {day}", + "friendship": "Venskab", + "add_source": "Tilføj Kilde", + "snapchat_plus": "Snapchat Plus", + "snapchat_plus_state": { + "subscribed": "Abonnement bekræftet", + "not_subscribed": "Ikke abonneret" + } + }, + "chat_export": { + "dialog_negative_button": "Annuller", + "dialog_positive_button": "Eksportér", + "exported_to": "Eksporteret til {path}", + "exporting_chats": "Eksporterer Chats...", + "processing_chats": "Behandler {amount} samtaler...", + "export_fail": "Kunne ikke eksportere samtale {conversation}", + "writing_output": "Skriver output...", + "finished": "Færdig! Du kan nu lukke denne dialog.", + "no_messages_found": "Ingen beskeder fundet!", + "exporting_message": "Eksporterer {conversation}..." + }, + "button": { + "ok": "OK", + "positive": "Ja", + "negative": "Nej", + "cancel": "Annuller", + "open": "Åbn", + "download": "Hent" + }, + "profile_picture_downloader": { + "button": "Download Profilbillede", + "title": "Profil Billede Downloader", + "avatar_option": "Avatar", + "background_option": "Baggrund" + }, + "download_processor": { + "attachment_type": { + "snap": "Snap", + "sticker": "Klistermærke", + "external_media": "Eksterne medier", + "note": "Note", + "original_story": "Oprindelig Historie" + }, + "select_attachments_title": "Vælg vedhæftede filer at downloade", + "download_started_toast": "Download startet", + "unsupported_content_type_toast": "Ikke-understøttet indholdstype!", + "failed_no_longer_available_toast": "Medier er ikke længere tilgængelige", + "no_attachments_toast": "Ingen vedhæftning fundet!", + "already_queued_toast": "Medie allerede i kø!", + "already_downloaded_toast": "Medier er allerede downloadet!", + "saved_toast": "Gemt i {path}", + "download_toast": "Downloader {path}...", + "processing_toast": "Behandler {path}...", + "failed_generic_toast": "Kunne ikke downloade", + "failed_to_create_preview_toast": "Kunne ikke oprette forhåndsvisning", + "failed_processing_toast": "Mislykkedes at behandle {error}", + "failed_gallery_toast": "Kunne ikke gemme i galleri {error}" + }, + "streaks_reminder": { + "notification_title": "Streaks", + "notification_text": "Du vil miste din Streak med {friend} på {hoursLeft} timer" + } +} diff --git a/common/src/main/assets/lang/de_DE.json b/common/src/main/assets/lang/de_DE.json index 7acae4fa0..f0552dcc2 100644 --- a/common/src/main/assets/lang/de_DE.json +++ b/common/src/main/assets/lang/de_DE.json @@ -1,329 +1,861 @@ { - "category": { - "spying_privacy": "Spying & Privatsphäre", - "media_manager": "Medien Verwaltung", - "ui_tweaks": "UI & Tweaks", - "camera": "Kamera", - "updates": "Updates", - "experimental_debugging": "Experimentell" - }, - "action": { - "clean_cache": "Cache bereinigen", - "clear_message_logger": "Nachrichten Logger bereinigen", - "refresh_mappings": "Mappings erneuern", - "open_map": "Standort auf der Map auswählen", - "check_for_updates": "Auf Updates überprüfen", - "export_chat_messages": "Chat Nachrichten Exportieren" - }, - "property": { - "message_logger": { - "name": "Nachrichten Logger", - "description": "Verhindern, dass Nachrichten gelöscht werden" - }, - "prevent_read_receipts": { - "name": "Lesebestätigungen verhindern", - "description": "Verhindern, dass jemand erfährt, dass ein Snap geöffnet wurde" - }, - "hide_bitmoji_presence": { - "name": "Bitmoji Präsenz verstecken", - "description": "Verstecke die Bitmoji-Präsenz im Chat" - }, - "better_notifications": { - "name": "Bessere Benachrichtigungen", - "description": "Zeige weitere Informationen in Benachrichtigungen an" - }, - "notification_blacklist": { - "name": "Benachrichtigungs Blacklist", - "description": "Blendet den ausgewählten Benachrichtigungstyp aus" - }, - "disable_metrics": { - "name": "Metriken deaktivieren", - "description": "Snapchat-Metriken deaktivieren" - }, - "block_ads": { - "name": "Werbung blockieren", - "description": "Blockiere das Anzeigen von Werbung" - }, - "unlimited_snap_view_time": { - "name": "Unbegrenztes Ansehen von Snaps", - "description": "Entfernt das Zeitlimit für die Anzeige von Snaps" - }, - "prevent_sending_messages": { - "name": "Unerwünschte Nachrichten abhalten", - "description": "Verhindert das Versenden bestimmter Nachrichten" - }, - "anonymous_story_view": { - "name": "Anonyme Story Ansicht", - "description": "Verhindert, dass jemand erfährt, dass seine Story gesehen wurde" - }, - "hide_typing_notification": { - "name": "Tippen-Benachrichtigung verbergen", - "description": "Verhindert das Senden von Schreibbenachrichtigungen" - }, - "save_folder": { - "name": "Speicherverzeichnis", - "description": "Der Ordner in dem alle Medien gespeichert werden" - }, - "auto_download_options": { - "name": "Auto Download Optionen", - "description": "Wähle aus welche Medien automatisch heruntergeladen werden sollen" - }, - "download_options": { - "name": "Download Optionen", - "description": "Gebe das Dateiformat an" - }, - "chat_download_context_menu": { - "name": "Kontextmenü für Chat-Download aktivieren", - "description": "Aktivieren Sie das Kontextmenü zum Chat-Download" - }, - "gallery_media_send_override": { - "name": "Überschreiben beim Senden von Gallerie Medien", - "description": "Überschreibt die von der Galerie gesendeten Medien" - }, - "auto_save_messages": { - "name": "Automatisches Speichern von Nachrichten", - "description": "Wähle aus welche Art von Nachrichten automatisch gespeichert werden sollen" - }, - "force_media_source_quality": { - "name": "Medien Qualität Überschreiben", - "description": "Überschreibt die Qualität der Medienquelle" - }, - "download_logging": { - "name": "Download Protokollierung", - "description": "Toast anzeigen, wenn Medien heruntergeladen werden" - }, - "enable_friend_feed_menu_bar": { - "name": "Freunde Feed Menüleiste", - "description": "Aktiviert die \"neue Freunde\" Menüleiste" - }, - "friend_feed_menu_buttons": { - "name": "Schaltflächen für das Freunde Feed Menü", - "description": "Wähle aus welche Schaltflächen in der Freunde Feed Menüleiste angezeigt werden sollen" - }, - "friend_feed_menu_buttons_position": { - "name": "Positionsindex der Freunde Feed Schaltflächen", - "description": "Die Position der Freunde Feed Menüschaltflächen" - }, - "hide_ui_elements": { - "name": "UI-Elemente ausblenden", - "description": "Wähle aus welche UI-Elemente ausgeblendet werden sollen" - }, - "hide_story_section": { - "name": "Story Abschnitt ausblenden", - "description": "Blenden Sie bestimmte im Story Bereich angezeigte UI-Elemente aus" - }, - "story_viewer_override": { - "name": "Story Viewer Überschreibung", - "description": "Aktiviert bestimmte Funktionen die Snapchat ausgeblendet hat" - }, - "streak_expiration_info": { - "name": "Informationen zum Streak Ablauf anzeigen", - "description": "Zeigt Informationen zum Streak Ablauf neben den Streaks an" - }, - "disable_snap_splitting": { - "name": "Deaktivieren der Snap-Aufteilung", - "description": "Verhindert, dass Snaps in mehrere Teile aufgeteilt werden" + "setup": { + "dialogs": { + "select_language": "Sprache auswählen", + "save_folder": "SnapEnhance benötigt Zugriff auf den Gerätespeicher, um Medien von Snapchat herunterzuladen und zu sichern.\nBitte wähle einen Ziel-Ordner für die Downloads aus.", + "select_save_folder_button": "Ordner wählen" }, - "disable_video_length_restriction": { - "name": "Deaktiviert die Beschränkung der Videolänge", - "description": "Deaktiviert die Beschränkung der Videolänge" + "mappings": { + "dialog": "Damit SnapEnhance eine Vielzahl von SnapChat-Versionen gleichzeitig unterstützen kann, sind Zuordnungen notwendig. Dies sollte nicht länger als 5 Sekunden dauern.", + "generate_button": "Erstellen", + "generate_failure_no_snapchat": "SnapEnhance konnte Snapchat nicht finden, bitte versuchen Sie Snapchat neu zu installieren.", + "generate_failure": "Beim Erstellen der Zuordnungen ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.", + "generate_success": "Die Zuordnungen wurden erfolgreich erstellt." }, - "snapchat_plus": { - "name": "Snapchat Plus", - "description": "Aktiviert Snapchat Plus-Funktionen" - }, - "new_map_ui": { - "name": "Neue Karten UI", - "description": "Aktiviert die neue Karten-Benutzeroberfläche" - }, - "location_spoof": { - "name": "Snapmap Standort Spoofer", - "description": "Verfälscht den Standort auf der Snapmap" - }, - "message_preview_length": { - "name": "Länge der Nachrichtenvorschau", - "description": "Gebe die Anzahl der Nachrichten an die in der Vorschau angezeigt werden sollen" - }, - "unlimited_conversation_pinning": { - "name": "Unbegrenztes Anpinnen von Unterhaltungen", - "description": "Aktiviert die Fähigkeit, unbegrenzte Unterhaltungen anzupinnen" - }, - "disable_spotlight": { - "name": "Deaktivieren des Spotlights", - "description": "Deaktiviert die Spotlight Seite" - }, - "enable_app_appearance": { - "name": "Aktivieret die App Darstellungseinstellungen", - "description": "Aktiviert die ausgeblendete Einstellung für die Darstellung der App" - }, - "startup_page_override": { - "name": "Änderung des Startbildschirms", - "description": "Verändert die Startseite" - }, - "disable_google_play_dialogs": { - "name": "Google Play-Service-Warnungen Dialog deaktivieren", - "description": "Verfügbarkeitsdialog für Google Play Services nicht anzeigen" - }, - "auto_updater": { - "name": "Auto Updater", - "description": "Der Intervall für die Suche nach Updates" - }, - "disable_camera": { - "name": "Kamera deaktivieren", - "description": "Verhindert dass Snapchat die Kamera verwenden kann" - }, - "immersive_camera_preview": { - "name": "Immersive Kamera-Vorschau", - "description": "Verhindert, dass Snapchat die Kameravorschau zuschneidet" - }, - "preview_resolution": { - "name": "Vorschauauflösung", - "description": "Überschreibt die Auflösung der Kameravorschau" - }, - "picture_resolution": { - "name": "Bildauflösung", - "description": "Überschreibt die Bildauflösung" - }, - "force_highest_frame_rate": { - "name": "Höchste Bildwiederholrate erzwingen", - "description": "Erzwingt die höchstmögliche Bildwiederholungsrate" - }, - "force_camera_source_encoding": { - "name": "Kodierung der Kameraquelle erzwingen", - "description": "Kodierung der Kameraquelle erzwingen" - }, - "app_passcode": { - "name": "Lege ein App Passwort fest", - "description": "Legt einen Passcode zum Entsperren der App fest" - }, - "app_lock_on_resume": { - "name": "App-Sperre beim Fortsetzen", - "description": "Sperrt die App, wenn sie erneut geöffnet wird" - }, - "infinite_story_boost": { - "name": "Unendlicher Story Boost", - "description": "Unendlicher boost für deine Story" - }, - "meo_passcode_bypass": { - "name": "Passwortumgehung für privaten Bereich", - "description": "Umgeht das Passwort für den privaten Ordner Funktioniert nur, wenn das richtige Passwort schon einmal eingegeben wurde" - }, - "amoled_dark_mode": { - "name": "AMOLED Dark Mode", - "description": "Aktiviert den AMOLED-Dunkelmodus\nStelle sicher, dass der Dunkelmodus von Snapchat aktiviert ist" - }, - "unlimited_multi_snap": { - "name": "Unbegrenzte Multi-Snaps", - "description": "Ermöglicht die Aufnahme einer unbegrenzten Anzahl von Multi-Snaps" + "permissions": { + "dialog": "Um fortfahren zu können, müssen Sie diese Anforderungen erfüllen:", + "notification_access": "Zugriff auf Benachrichtigungen", + "battery_optimization": "Batterie Optimierung", + "display_over_other_apps": "Über anderen Apps einblenden", + "request_button": "Anfordern" + } + }, + "manager": { + "routes": { + "features": "Features", + "home": "Startseite", + "home_settings": "Einstellungen", + "home_logs": "Logs", + "social": "Social", + "scripts": "Skripte", + "tasks": "Aufgaben" }, - "device_spoof": { - "name": "Device Values Spoofen", - "description": "Spoofed bestimmte Werte des Geräts" + "sections": { + "home": { + "logs": { + "clear_logs_button": "Logs löschen", + "export_logs_button": "Logs exportieren" + } + }, + "features": { + "disabled": "Deaktiviert" + }, + "social": { + "e2ee_title": "Ende-zu-Ende-Verschlüsselung", + "rules_title": "Regeln", + "participants_text": "{count} Teilnehmer", + "not_found": "Nicht gefunden", + "streaks_title": "Flammen", + "streaks_length_text": "Dauer: {length}", + "streaks_expiration_short": "{hours}h", + "streaks_expiration_text": "Läuft in {eta} ab", + "reminder_button": "Erinnerung festlegen" + } }, - "device_fingerprint": { - "name": "Gerät Fingerabdruck", - "description": "Spoofed den Geräte-Fingerabdruck" + "dialogs": { + "add_friend": { + "title": "Freund oder Gruppe hinzufügen", + "search_hint": "Suchen", + "fetch_error": "Fehler beim Abrufen der Daten", + "category_groups": "Gruppen", + "category_friends": "Freunde" + }, + "scripting_warning": { + "content": "SnapEnhance enthält ein Skripting-Tool, das die Ausführung von benutzerdefinierten Code auf Ihrem Gerät ermöglicht. Seien Sie äußerst vorsichtig und installieren Sie nur Module aus bekannten, zuverlässigen Quellen. Unautorisierte oder ungeprüfte Module können Sicherheitsrisiken für Ihr System darstellen.", + "title": "Warnung" + } + } + }, + "rules": { + "modes": { + "blacklist": "Schwarzlisten-Modus", + "whitelist": "Weißlisten-Modus" }, - "android_id": { - "name": "Android ID", - "description": "Spoofed die Android-ID" + "properties": { + "auto_download": { + "name": "Auto-Download", + "description": "Snaps beim Ansehen automatisch herunterladen", + "options": { + "blacklist": "Vom Auto-Download ausschließen", + "whitelist": "Auto-Download" + } + }, + "stealth": { + "name": "Heimlicher Modus", + "description": "Verhindert, dass jemand weiß, dass du seine Snaps/Chats oder Gespräche geöffnet hast", + "options": { + "blacklist": "Vom Heimlichen Modus Ausschließen", + "whitelist": "Heimlicher Modus" + } + }, + "auto_save": { + "name": "Automatisch speichern", + "description": "Speichert Chat-Nachrichten beim Ansehen", + "options": { + "blacklist": "Vom automatischen Speichern ausschließen", + "whitelist": "Automatisch speichern" + } + }, + "hide_friend_feed": { + "name": "Vom Freundes-Feed ausblenden" + }, + "e2e_encryption": { + "name": "E2E-Verschlüsselung verwenden" + }, + "pin_conversation": { + "name": "Unterhaltung anheften" + }, + "unsaveable_messages": { + "name": "Nicht speicherbare Nachrichten", + "options": { + "blacklist": "Von nicht speicherbaren Nachrichten ausschließen", + "whitelist": "Nicht speicherbare Nachrichten" + }, + "description": "Verhindert, dass Nachrichten im Chat von anderen Personen gespeichert werden können" + } } }, - "option": { - "property": { + "actions": { + "clean_snapchat_cache": "Snapchat-Cache leeren", + "clear_message_logger": "Nachrichten Logger löschen", + "refresh_mappings": "Mappings erneuern", + "open_map": "Standort auf der Karte wählen", + "check_for_updates": "Auf Updates überprüfen", + "export_chat_messages": "Chat Nachrichten Exportieren", + "bulk_messaging_action": "Massen Aktion" + }, + "features": { + "notices": { + "unstable": "⚠ Instabil", + "ban_risk": "⚠ Dieses Feature kann zu Bans führen", + "internal_behavior": "⚠ Dies kann das interne Verhalten von Snapchat stören", + "require_native_hooks": "⚠ Um korrekt zu funktionieren erfordert dieses Feature experimentelle Native Hooks" + }, + "properties": { + "downloader": { + "name": "Herunterlader", + "description": "Snapchat Medien herunterladen", + "properties": { + "save_folder": { + "name": "Speicherverzeichnis", + "description": "Wähle das Verzeichnis, in das alle Medien heruntergeladen werden sollen" + }, + "auto_download_sources": { + "name": "Quellen automatisch herunterladen", + "description": "Wähle die Quellen, von denen automatisch herunterzuladen ist" + }, + "prevent_self_auto_download": { + "name": "Selbst-Auto-Download verhindern", + "description": "Verhindert, dass eigene Snaps automatisch heruntergeladen werden" + }, + "path_format": { + "name": "Pfadformat", + "description": "Gib das Dateiformat an" + }, + "allow_duplicate": { + "name": "Duplikate erlauben", + "description": "Ermöglicht es, dass dieselben Medien mehrmals heruntergeladen werden" + }, + "merge_overlays": { + "name": "Overlays zusammenführen", + "description": "Kombiniert den Text und die Medien eines Snaps in eine Datei" + }, + "force_image_format": { + "name": "Bildformat erzwingen", + "description": "Erzwingt das Speichern von Bildern in einem bestimmten Format" + }, + "force_voice_note_format": { + "name": "Sprachnotiz Format erzwingen", + "description": "Erzwingt das Speichern von Sprachnotizen in einem bestimmten Format" + }, + "download_profile_pictures": { + "name": "Profilbilder herunterladen", + "description": "Ermöglicht das Herunterladen von Profilbildern von einer Profilseite" + }, + "chat_download_context_menu": { + "name": "Chat Download Context Menü", + "description": "Ermöglicht das Herunterladen von Medien aus einer Unterhaltung durch langes Drücken" + }, + "ffmpeg_options": { + "name": "FFmpeg-Optionen", + "description": "Zusätzliche FFmpeg-Optionen angeben", + "properties": { + "threads": { + "name": "Threads", + "description": "Die Anzahl Threads, welche zu gebrauchen ist" + }, + "preset": { + "name": "Voreinstellungen", + "description": "Geschwindigkeit der Konvertierung festlegen" + }, + "constant_rate_factor": { + "description": "Setze den Constant Rate Factor für den Video-Encoder\nvon 0 bis 51 für libx264", + "name": "Konstanter Rate-Faktor" + }, + "video_bitrate": { + "name": "Videobitrate", + "description": "Video-Bitrate (kbps) festlegen" + }, + "audio_bitrate": { + "name": "Audiobitrate", + "description": "Audio-Bitrate (kbps) festlegen" + }, + "custom_video_codec": { + "name": "Benutzerdefinierter Video-Codec", + "description": "Wähle einen benutzerdefinierten Video-Codec (z.B. libx264)" + }, + "custom_audio_codec": { + "name": "Benutzerdefinierter Audio-Codec", + "description": "Wähle einen benutzerdefinierten Audio-Codec (z.B. AAC)" + } + } + }, + "logging": { + "name": "Logging", + "description": "Zeigt Toasts, wenn Medien heruntergeladen werden" + }, + "custom_path_format": { + "description": "Legen Sie ein benutzerdefiniertes Pfadformat für heruntergeladene Medien fest\n\nVerfügbare Variablen:\n - %username%\n - %source%\n - %hash%\n - %date_time%", + "name": "Benutzerdefiniertes Pfadformat" + }, + "opera_download_button": { + "description": "Fügt einen Download-Button in der oberen rechten Ecke hinzu, wenn ein Snap angezeigt wird", + "name": "Schwebender Download Button" + } + } + }, + "user_interface": { + "name": "Benutzeroberfläche", + "description": "Ändere das Aussehen von Snapchat", + "properties": { + "enable_app_appearance": { + "name": "Aktiviert die App Darstellungseinstellungen", + "description": "Aktiviert die versteckte App-Erscheinungsbild Einstellung,\nbei neueren Snapchat-Versionen möglicherweise nicht erforderlich" + }, + "amoled_dark_mode": { + "name": "AMOLED Dark Mode", + "description": "Aktiviert den AMOLED Dark Mode\nStelle sicher, dass der Dunkelmodus von Snapchat aktiviert ist" + }, + "friend_feed_message_preview": { + "name": "Freund Feed Nachrichten Vorschau", + "description": "Zeigt eine Vorschau der letzten Nachrichten im Freundes-Feed", + "properties": { + "amount": { + "name": "Anzahl", + "description": "Die Anzahl der Nachrichten, die in der Vorschau angezeigt werden" + } + } + }, + "bootstrap_override": { + "name": "Bootstrap Überschreibung", + "description": "Bootstrap-Einstellungen der Benutzeroberfläche überschreiben", + "properties": { + "app_appearance": { + "name": "App-Erscheinungsbild", + "description": "Legt eine dauerhafte App-Darstellung fest" + }, + "home_tab": { + "name": "Home Registerkarte", + "description": "Überschreibt den Start-Tab beim Öffnen von Snapchat" + } + } + }, + "map_friend_nametags": { + "name": "Verbesserte Karten-Namensschilder von Freunden", + "description": "Verbessert die Namensschilder von Freunden auf der Snapmap" + }, + "streak_expiration_info": { + "name": "Informationen zum Flammen-Ablauf anzeigen", + "description": "Zeigt einen Flammen-Ablauf-Timer neben dem Flammen-Zähler" + }, + "hide_friend_feed_entry": { + "name": "Freund Feed Eintrag ausblenden", + "description": "Versteckt einen bestimmten Freund aus dem Freundes-Feed,\nBenutze den sozialen Tab um diese Funktion zu verwalten" + }, + "hide_streak_restore": { + "name": "Flammen-Wiederherstellung verstecken", + "description": "Versteckt den Wiederherstellen-Button im Freundesfeed" + }, + "hide_story_sections": { + "name": "Story Abschnitt ausblenden", + "description": "Blendet bestimmte, im Story Bereich angezeigte, UI-Elemente aus" + }, + "hide_ui_components": { + "name": "UI-Komponenten ausblenden", + "description": "Wähle aus welche UI-Elemente ausgeblendet werden sollen" + }, + "disable_spotlight": { + "name": "Spotlight deaktivieren", + "description": "Deaktiviert die Spotlight Seite" + }, + "friend_feed_menu_buttons": { + "name": "Schaltflächen für das Freunde-Feed Menü", + "description": "Wähle aus welche Schaltflächen in der Freunde Feed Menüleiste angezeigt werden sollen" + }, + "friend_feed_menu_position": { + "name": "Positionsindex des Freunde Feeds", + "description": "Die Position der Freunde Feed Komponente" + }, + "enable_friend_feed_menu_bar": { + "name": "Freunde Feed Menüleiste", + "description": "Aktiviert die neue Freunde Feed Menüleiste" + }, + "fidelius_indicator": { + "name": "Indikator für Einzigartigkeit", + "description": "Zeigt einen grünen Kreis neben Nachrichten, die nur an Sie gesendet wurden" + }, + "hide_settings_gear": { + "name": "Symbol für Einstellungen ausblenden", + "description": "Blendet das SnapEnhance-Einstellungssymbol im Chat-Tab aus" + }, + "opera_media_quick_info": { + "description": "Zeigt nützliche Informationen zu Medien wie das Erstellungsdatum im Kontextmenü der Snap-Ansicht an", + "name": "Medien Schnellinfo" + }, + "vertical_story_viewer": { + "name": "Vertikale Story Ansicht", + "description": "Aktiviert die vertikale Story Ansicht für alle Storys" + }, + "old_bitmoji_selfie": { + "name": "Altes Bitmoji-Selfie", + "description": "Bringt die Bitmoji-Selfies aus früheren Snapchat-Versionen zurück" + }, + "prevent_message_list_auto_scroll": { + "name": "Automatisches Scrollen der Nachrichtenliste verhindern", + "description": "Verhindert, dass die Nachrichtenliste beim Senden/Empfangen einer Nachricht nach unten scrollt" + }, + "edit_text_override": { + "name": "Textfeld-Verhalten überschreiben", + "description": "Überschreibt das Verhalten von Textfeldern" + }, + "snap_preview": { + "name": "Snap-Vorschau", + "description": "Zeigt eine kleine Vorschau neben ungesehenen Snaps im Chat an" + }, + "hide_quick_add_friend_feed": { + "description": "Blendet den Abschnitt Schnelles Hinzufügen im Freundes-Feed aus", + "name": "Schnelles Hinzufügen im Friend Feed ausblenden" + } + } + }, + "messaging": { + "name": "Mitteilungen", + "description": "Ändern wie mit Freunden interagiert wird", + "properties": { + "anonymous_story_viewing": { + "name": "Anonyme Story Ansicht", + "description": "Verhindert, dass jemand erfährt, dass du seine Story gesehen hast" + }, + "hide_bitmoji_presence": { + "name": "Bitmoji Präsenz verstecken", + "description": "Verhindert, dass dein Bitmoji im Chat auftaucht" + }, + "hide_typing_notifications": { + "name": "Tippen-Benachrichtigungen verbergen", + "description": "Verhindert, dass jemand erfährt, dass du eine Nachricht tippst" + }, + "unlimited_snap_view_time": { + "name": "Unbegrenzte Zeit zum Ansehen von Snaps", + "description": "Entfernt das Zeitlimit für die Anzeige von Snaps" + }, + "disable_replay_in_ff": { + "name": "Replay in FF deaktivieren", + "description": "Deaktiviert die Möglichkeit, mit einem langen Drücken vom Freundes-Feed zu wiederholen" + }, + "message_preview_length": { + "name": "Länge der Nachrichtenvorschau", + "description": "Gib die Anzahl der Nachrichten an, die in der Vorschau angezeigt werden sollen" + }, + "prevent_message_sending": { + "name": "Nachrichtenversand verhindern", + "description": "Verhindert das Versenden bestimmter Nachrichten" + }, + "better_notifications": { + "name": "Bessere Benachrichtigungen", + "description": "Zeige weitere Informationen in Benachrichtigungen an" + }, + "notification_blacklist": { + "name": "Benachrichtigungs Blacklist", + "description": "Wählen Sie Benachrichtigungen aus, die blockiert werden sollen" + }, + "message_logger": { + "name": "Nachrichten Logger", + "description": "Verhindert, dass Nachrichten gelöscht werden", + "properties": { + "message_filter": { + "name": "Nachrichtenfilter", + "description": "Wählen Sie aus, welche Nachrichten behalten werden sollen (leer für alle Nachrichten)" + }, + "auto_purge": { + "description": "Löscht automatisch zwischengespeicherte Nachrichten, die älter als die angegebene Zeit sind", + "name": "Automatische Bereinigung" + }, + "keep_my_own_messages": { + "name": "Eigene Nachrichten behalten", + "description": "Verhindert, dass Ihre eigenen Nachrichten gelöscht werden" + } + } + }, + "auto_save_messages_in_conversations": { + "name": "Automatisches Speichern von Nachrichten", + "description": "Speichert automatisch jede Nachricht in Unterhaltungen" + }, + "gallery_media_send_override": { + "name": "Galerie-Medien senden Überschreiben", + "description": "Fälscht die Medienquelle, wenn etwas von der Galerie gesendet wird" + }, + "bypass_screenshot_detection": { + "description": "Verhindert, dass Snapchat erkennt, wenn du einen Screenshot machst", + "name": "Umgehen der Screenshot-Erkennung" + }, + "half_swipe_notifier": { + "name": "Über Half-Swipes informieren", + "description": "Benachrichtigt Sie, wenn jemand halb in ihren Chat swiped", + "properties": { + "min_duration": { + "name": "Mindestdauer", + "description": "Die Mindestdauer der halben Swipes (in Sekunden)" + }, + "max_duration": { + "description": "Die maximale Dauer des halben Swipes (in Sekunden)", + "name": "Maximale Dauer" + } + } + }, + "prevent_story_rewatch_indicator": { + "name": "Wiederholungs-Indikator bei Stories verhindern", + "description": "Verhindert, dass andere wissen, dass Sie ihre Story noch einmal angeschaut haben" + }, + "hide_peek_a_peek": { + "description": "Verhindert, dass eine Benachrichtigung gesendet wird, wenn Sie halb in einen Chat swipen", + "name": "Vorschau Benachrichtigung verhindern" + }, + "strip_media_metadata": { + "description": "Entfernt Metadaten von Medien vor dem Versand als Nachricht", + "name": "Medien-Metadaten entfernen" + }, + "bypass_message_retention_policy": { + "name": "Umgehung der Richtlinie zur Aufbewahrung von Nachrichten", + "description": "Verhindert, dass Nachrichten nach dem Anzeigen gelöscht werden" + }, + "call_start_confirmation": { + "name": "Bestätigung des Starts eines Anrufs", + "description": "Zeigt einen Bestätigungsdialog beim Starten eines Anrufs an" + }, + "instant_delete": { + "name": "Sofortiges Löschen", + "description": "Entfernt den Bestätigungsdialog beim Löschen von Nachrichten" + } + } + }, + "global": { + "name": "Global", + "description": "Globale Snapchat-Einstellungen anpassen", + "properties": { + "spoofLocation": { + "name": "Standort", + "description": "Fälsch deinen Standort", + "properties": { + "coordinates": { + "name": "Koordinaten", + "description": "Koordinaten festlegen" + } + } + }, + "snapchat_plus": { + "name": "Snapchat Plus", + "description": "Aktiviert Snapchat Plus Funktionen\nEinige serverseitige Funktionen funktionieren möglicherweise nicht" + }, + "auto_updater": { + "name": "Auto Updater", + "description": "Automatisch auf Updates prüfen" + }, + "disable_metrics": { + "name": "Metriken deaktivieren", + "description": "Verhindert das Senden von analytischen Daten an Snapchat" + }, + "block_ads": { + "name": "Werbung blockieren", + "description": "Verhindert die Anzeige von Werbung" + }, + "bypass_video_length_restriction": { + "name": "Umgeht Videolängenbeschränkungen", + "description": "Einzel: sendet ein einzelnes Video\nSplitt: Videos nach Bearbeitung aufteilen" + }, + "disable_google_play_dialogs": { + "name": "Google Play-Service Dialog deaktivieren", + "description": "Verfügbarkeitsdialog für Google Play Services nicht anzeigen" + }, + "disable_snap_splitting": { + "name": "Snap-Aufteilung deaktivieren", + "description": "Verhindert, dass Snaps in mehrere Teile aufgeteilt werden\nBilder werden in Videos umgewandelt" + }, + "disable_confirmation_dialogs": { + "name": "Bestätigungsdialoge deaktivieren", + "description": "Bestätigt automatisch ausgewählte Aktionen" + }, + "suspend_location_updates": { + "name": "Standortaktualisierungen aussetzen", + "description": "Fügt einen Button in den Karteneinstellungen hinzu, um Standortaktualisierungen auszusetzen" + }, + "spotlight_comments_username": { + "name": "Spotlight Kommentare Benutzername", + "description": "Zeigt den Benutzernamen des Autors in Spotlight-Kommentaren an" + }, + "disable_public_stories": { + "name": "Öffentliche Stories deaktivieren", + "description": "Entfernt jede öffentliche Story von der Discover-Seite\nErfordert möglicherweise einen leeren Cache, um richtig zu funktionieren" + }, + "force_upload_source_quality": { + "name": "Upload-Qualität erzwingen", + "description": "Zwingt Snapchat dazu, Medien in der Originalqualität hochzuladen\nBitte beachten Sie, dass dadurch möglicherweise keine Metadaten aus den Medien entfernt werden" + } + } + }, + "rules": { + "name": "Regeln", + "description": "Automatische Funktionen für einzelne Personen verwalten" + }, + "camera": { + "name": "Kamera", + "description": "Pass die richtigen Einstellungen für den perfekten Snap an", + "properties": { + "disable_camera": { + "name": "Kamera deaktivieren", + "description": "Verhindert die Nutzung der Kameras durch Snapchat" + }, + "immersive_camera_preview": { + "name": "Immersive Vorschau", + "description": "Verhindert das Beschneiden der Kameravorschau\nDas kann dazu führen, dass die Kamera auf einigen Geräten flickert" + }, + "override_preview_resolution": { + "name": "Vorschauauflösung überschreiben", + "description": "Überschreibt die Auflösung der Kameravorschau" + }, + "override_picture_resolution": { + "name": "Überschreiben der Bildauflösung", + "description": "Überschreibt die Bildauflösung" + }, + "custom_frame_rate": { + "name": "Benutzerdefinierte Frame Rate", + "description": "Überschreibt die Frame Rate der Kamera" + }, + "force_camera_source_encoding": { + "name": "Kodierung der Kameraquelle erzwingen", + "description": "Erzwingt die Kodierung der Kameraquelle" + }, + "custom_preview_resolution": { + "description": "Legt eine benutzerdefinierte Auflösung für die Kameravorschau fest, Breite x Höhe (z. B. 1920x1080).\nDie benutzerdefinierte Auflösung muss von Ihrem Gerät unterstützt werden", + "name": "Benutzerdefinierte Auflösung der Vorschau" + }, + "hevc_recording": { + "name": "HEVC Aufnahme", + "description": "Verwendet HEVC (H.265) Codec für die Videoaufzeichnung" + }, + "custom_picture_resolution": { + "description": "Legt eine benutzerdefinierte Bildauflösung fest, Breite x Höhe (z. B. 1920x1080).\nDie benutzerdefinierte Auflösung muss von Ihrem Gerät unterstützt werden", + "name": "Benutzerdefinierte Bildauflösung" + }, + "black_photos": { + "description": "Ersetzt die aufgenommenen Fotos durch einen schwarzen Hintergrund\nVideos sind davon nicht betroffen", + "name": "Schwarze Fotos" + } + } + }, + "streaks_reminder": { + "name": "Flammen-Erinnerung", + "description": "Benachrichtigt dich regelmäßig über deine Flammen", + "properties": { + "interval": { + "name": "Intervall", + "description": "Das Intervall zwischen jeder Erinnerung (Stunden)" + }, + "remaining_hours": { + "name": "Verbleibende Zeit", + "description": "Die verbleibende Zeit, bevor die Benachrichtigung angezeigt wird" + }, + "group_notifications": { + "name": "Gruppierte Benachrichtigungen", + "description": "Benachrichtigungen in eine einzelne gruppieren" + } + } + }, + "experimental": { + "name": "Experimentell", + "description": "Experimentelle Funktionen", + "properties": { + "native_hooks": { + "name": "Native Hooks", + "description": "Unsichere Funktionen, welche sich in Snapchats nativen Code einhängen", + "properties": { + "disable_bitmoji": { + "name": "Bitmojis deaktivieren", + "description": "Deaktiviert das Freundesprofil Bitmoji" + } + } + }, + "spoof": { + "name": "Verfälschen", + "description": "Verschiedene Informationen über dich verfälschen", + "properties": { + "randomize_persistent_device_token": { + "description": "Erzeugt ein zufälliges Geräte-Token nach jedem Login", + "name": "Dauerhaftes Geräte-Token zufällig auswählen" + }, + "remove_mock_location_flag": { + "name": "Kennzeichnung für den gefälschte Standort entfernen", + "description": "Verhindert, dass Snapchat gefälschte Standorte erkennt" + }, + "android_id": { + "name": "Android ID", + "description": "Fälscht Ihre Android ID mit dem angegebenen Wert" + }, + "fingerprint": { + "description": "Fälscht den Fingerabdruck Ihres Geräts", + "name": "Geräte-Fingerabdruck" + }, + "remove_vpn_transport_flag": { + "description": "Hindert Snapchat daran, VPNs zu erkennen", + "name": "VPN-Transport-Flag entfernen" + }, + "play_store_installer_package_name": { + "description": "Überschreibt den Namen des Installationspakets auf com.android.vending", + "name": "Play Store Installer Paketname" + } + } + }, + "app_passcode": { + "name": "App-Passcode", + "description": "Legt einen Passcode zum Entsperren der App fest" + }, + "app_lock_on_resume": { + "name": "App-Sperre beim Fortsetzen", + "description": "Sperrt die App, wenn sie erneut geöffnet wird" + }, + "infinite_story_boost": { + "name": "Unendlicher Story Boost", + "description": "Story Boost Limit Verzögerung umgehen" + }, + "meo_passcode_bypass": { + "name": "Passwortumgehung für privaten Bereich", + "description": "Umgeht das Passwort für den privaten Bereich\nFunktioniert nur, wenn das korrekte Passwort schon einmal eingegeben wurde" + }, + "unlimited_multi_snap": { + "name": "Unbegrenzte Multi-Snaps", + "description": "Ermöglicht die Aufnahme einer unbegrenzten Anzahl von Multi-Snaps" + }, + "no_friend_score_delay": { + "name": "Keine Friend Score Delay", + "description": "Entfernt die Verzögerung beim Betrachten eines Friend Scores" + }, + "e2ee": { + "name": "Ende-zu-Ende-Verschlüsselung", + "description": "Verschlüsselt Ihre Nachrichten mit AES mithilfe eines gemeinsamen geheimen Schlüssels\nStell sicher, dass du den Schlüssel sicher lagerst!", + "properties": { + "encrypted_message_indicator": { + "name": "Verschlüsselte Nachricht Indikator", + "description": "Fügt einen 🔒 Emoji neben verschlüsselten Nachrichten hinzu" + }, + "force_message_encryption": { + "name": "Nachrichtenverschlüsselung erzwingen", + "description": "Verhindert das Senden von verschlüsselten Nachrichten an Personen, die keine E2E-Verschlüsselung aktiviert haben, wenn mehrere Unterhaltungen ausgewählt sind" + } + } + }, + "add_friend_source_spoof": { + "name": "Freundesquellen-Spoof hinzufügen", + "description": "Verfälscht die Quelle einer Freundschaftsanfrage" + }, + "hidden_snapchat_plus_features": { + "name": "Versteckte Snapchat Plus-Funktionen", + "description": "Aktiviert unveröffentlichte/beta Snapchat Plus Funktionen\nKönnte auf älteren Snapchat-Versionen nicht funktionieren" + }, + "disable_composer_modules": { + "name": "Composer-Module deaktivieren", + "description": "Verhindert, dass ausgewählte Composer-Module geladen werden\nDie Namen müssen durch ein Komma getrennt werden" + }, + "prevent_forced_logout": { + "name": "Erzwungenen Logout verhindern", + "description": "Verhindert, dass Snapchat dich abmeldet, wenn du dich auf einem anderen Gerät anmeldest" + }, + "convert_message_locally": { + "description": "Konvertiert Snaps lokal in externe Chat-Medien. Dies erscheint im Kontextmenü für den Chat-Download", + "name": "Nachricht lokal konvertieren" + }, + "story_logger": { + "description": "Liefert eine Historie der Stories von Freunden", + "name": "Story Logger" + } + } + }, + "scripting": { + "name": "Scripting", + "description": "Benutzerdefinierte Skripte ausführen, um SnapEnhance zu erweitern", + "properties": { + "developer_mode": { + "name": "Entwicklermodus", + "description": "Zeigt Debug-Informationen auf Snapchat's UI" + }, + "module_folder": { + "name": "Modulordner", + "description": "Der Ordner, in dem sich die Skripte befinden" + }, + "integrated_ui": { + "name": "Integrierte Benutzeroberfläche", + "description": "Erlaubt Skripten, benutzerdefinierte UI-Komponenten zu Snapchat hinzuzufügen" + }, + "disable_log_anonymization": { + "description": "Deaktiviert die Anonymisierung von Logs", + "name": "Log-Anonymisierung deaktivieren" + }, + "auto_reload": { + "description": "Automatisches Neuladen von Skripten, wenn diese sich ändern", + "name": "Automatisches Neuladen" + } + } + } + }, + "options": { + "app_appearance": { + "always_light": "Immer hell", + "always_dark": "Immer dunkel" + }, "better_notifications": { - "chat": "Chatnachrichten anzeigen", - "snap": "Medien anzeigen", "reply_button": "Füge einen \"Antworten\" Knopf hinzu", - "download_button": "Zeige Download-Button" + "download_button": "Füge einen \"Herunterladen\" Knopf hinzu", + "group": "Benachrichtigungen gruppieren", + "mark_as_read_and_save_in_chat": "Im Chat speichern, wenn als gelesen markiert (hängt von der automatischen Speicherung ab)", + "mark_as_read_button": "Als gelesen markieren Button", + "media_preview": "Eine Vorschau der Medien anzeigen", + "chat_preview": "Eine Vorschau des Chats anzeigen" }, "friend_feed_menu_buttons": { - "auto_download_blacklist": "⬇️ Automatische Download-Blacklist", - "anti_auto_save": "💬 Anti-Auto-Nachricht-Speichern", - "stealth_mode": "👻 Heimlicher Modus", - "conversation_info": "👤 Gesprächsinformationen" - }, - "download_options": { - "allow_duplicate": "Erlaube doppelte Downloads", - "create_user_folder": "Erstelle einen Ordner für jeden Benutzer", - "append_hash": "Fügt dem Dateinamen einen einzigartigen Hash hinzu", - "append_username": "Füge den Benutzername zum Dateiname hinzu", - "append_date_time": "Füge Datum und Uhrzeit zum Dateinamen hinzu", - "append_type": "Füge den Medientyp zum Dateiname hinzu", - "merge_overlay": "Snap-Bild-Overlays zusammenführen" - }, - "auto_download_options": { + "auto_download": "⬇️ Auto-Download", + "auto_save": "💬 Auto-Nachricht-Speichern", + "stealth": "👻 Heimlicher Modus", + "conversation_info": "👤 Gesprächsinformationen", + "e2e_encryption": "🔒 E2E-Verschlüsselung verwenden", + "mark_stories_as_seen_locally": "👀 Stories als lokal gesehen markieren", + "mark_snaps_as_seen": "👀 Snaps als gesehen markieren", + "unsaveable_messages": "⬇️ Nicht speicherbare Nachrichten" + }, + "path_format": { + "create_author_folder": "Erzeuge ein Verzeichnis für jeden Benutzer", + "create_source_folder": "Ordner für jeden Medienquellentyp erstellen", + "append_hash": "Fügt jedem Dateinamen einen einzigartigen Hash hinzu", + "append_source": "Füge die Medienquelle zum Dateinamen hinzu", + "append_username": "Füge den Benutzernamen zum Dateinamen hinzu", + "append_date_time": "Füge Datum und Uhrzeit zum Dateinamen hinzu" + }, + "auto_download_sources": { "friend_snaps": "Freund-Snaps", - "friend_stories": "Freund Stories", + "friend_stories": "Freund-Stories", "public_stories": "Öffentliche Stories", "spotlight": "Spotlight" }, - "download_logging": { + "logging": { "started": "Gestartet", - "success": "Abgeschlossen", + "success": "Erfolgreich", "progress": "Fortschritt", "failure": "Fehler" }, - "auto_save_messages": { - "NOTE": "Audio Hinweis", - "CHAT": "Chat", - "EXTERNAL_MEDIA": "Externe Medien", - "SNAP": "Snap", - "STICKER": "Sticker" - }, "notifications": { "chat_screenshot": "Screenshot", "chat_screen_record": "Bildschirmaufnahme", - "camera_roll_save": "Camera Roll speichern", + "snap_replay": "Snap Wiederholung", + "camera_roll_save": "In Aufnahmen gespeichert", "chat": "Chat", "chat_reply": "Chat Antwort", "snap": "Snap", - "typing": "Typing", + "typing": "Schreiben", "stories": "Stories", + "chat_reaction": "DM-Reaktion", + "group_chat_reaction": "Gruppenreaktion", "initiate_audio": "Eingehender Audioanruf", - "abandon_audio": "Audioanruf in Abwesenheit", + "abandon_audio": "Verpasster Audioanruf", "initiate_video": "Eingehender Videoanruf", "abandon_video": "Verpasster Videoanruf" }, "gallery_media_send_override": { "ORIGINAL": "Original", - "NOTE": "Audio Hinweis", + "NOTE": "Audio-Notiz", "SNAP": "Snap", - "LIVE_SNAP": "Snap mit ton" - }, - "hide_ui_elements": { - "remove_call_buttons": "Entferne anruf buttons", - "remove_cognac_button": "Cognac-Button entfernen", - "remove_live_location_share_button": "Schaltfläche Live-Standortfreigabe entfernen", - "remove_stickers_button": "Entferne Sticker Button", - "remove_voice_record_button": "Knopf für Sprachaufzeichnung entfernen", - "remove_camera_borders": "Kameraränder entfernen" - }, - "auto_updater": { - "DISABLED": "Aus", - "EVERY_LAUNCH": "Bei jedem Start", - "DAILY": "Täglich", - "WEEKLY": "Wöchentlich" - }, - "story_viewer_override": { - "OFF": "Aus", - "DISCOVER_PLAYBACK_SEEKBAR": "Aktivieren Sie die Discovery Playback-Suchleiste", - "VERTICAL_STORY_VIEWER": "Aktiviere den vertikalen Story Viewer" - }, - "hide_story_section": { + "SAVABLE_SNAP": "Speicherbarer Snap" + }, + "hide_ui_components": { + "hide_profile_call_buttons": "Entferne Anruf Tasten", + "hide_chat_call_buttons": "Entferne Anruf Tasten im Chat", + "hide_live_location_share_button": "Schaltfläche Live-Standortfreigabe entfernen", + "hide_stickers_button": "Entferne Stickers Taste", + "hide_voice_record_button": "Knopf für Sprachaufzeichnung entfernen", + "hide_unread_chat_hint": "Hinweis auf ungelesene Chats entfernen" + }, + "hide_story_sections": { "hide_friend_suggestions": "Freundschaftsvorschläge verstecken", "hide_friends": "Freundesbereich ausblenden", - "hide_following": "Folgenden Abschnitt ausblenden", - "hide_for_you": "For You abschnitt ausblenden" - }, - "startup_page_override": { - "OFF": "Aus", - "ngs_map_icon_container": "Karte", - "ngs_chat_icon_container": "Chat", - "ngs_camera_icon_container": "Kamera", - "ngs_community_icon_container": "Community / Stories", - "ngs_spotlight_icon_container": "Spotlight", - "ngs_search_icon_container": "Suche" + "hide_suggested": "Verstecke empfohlen Sektion", + "hide_for_you": "For You Abschnitt ausblenden", + "hide_suggested_friend_stories": "Stories von empfohlenen Freunden ausblenden" + }, + "home_tab": { + "map": "Karte", + "chat": "Chat", + "camera": "Kamera", + "discover": "Entdecken", + "spotlight": "Spotlight" + }, + "add_friend_source_spoof": { + "added_by_username": "Nach Benutzername", + "added_by_mention": "Durch Erwähnung", + "added_by_group_chat": "Per Gruppenchat", + "added_by_qr_code": "Per QR-Code", + "added_by_community": "Per Community", + "added_by_quick_add": "Durch Schnelles Hinzufügen" + }, + "bypass_video_length_restriction": { + "single": "Einzelnes Medium", + "split": "Medien aufteilen" + }, + "auto_reload": { + "snapchat_only": "Nur Snapchat", + "all": "Alles (Snapchat + SnapEnhance)" + }, + "strip_media_metadata": { + "remove_audio_note_duration": "Dauer der Sprachnachricht entfernen", + "remove_audio_note_transcript_capability": "Sprachnachricht-Transkriptionsfunktion entfernen", + "hide_extras": "Extras ausblenden (z. B. Erwähnungen)", + "hide_caption_text": "Bildunterschriftstext ausblenden", + "hide_snap_filters": "Snap Filter ausblenden" + }, + "auto_purge": { + "1_day": "1 Tag", + "1_week": "1 Woche", + "1_month": "1 Monat", + "2_weeks": "2 Wochen", + "never": "Nie", + "1_hour": "1 Stunde", + "3_hours": "3 Stunden", + "6_months": "6 Monate", + "3_days": "3 Tage", + "6_hours": "6 Stunden", + "3_months": "3 Monate", + "12_hours": "12 Stunden" + }, + "disable_confirmation_dialogs": { + "hide_conversation": "Konversation ausblenden", + "clear_conversation": "Konversation aus dem Freundes-Feed löschen", + "remove_friend": "Freund entfernen", + "hide_friend": "Freund ausblenden", + "ignore_friend": "Freund ignorieren", + "block_friend": "Freund blockieren" + }, + "edit_text_override": { + "bypass_text_input_limit": "Umgehen des Limits für die Texteingabe", + "multi_line_chat_input": "Mehrzeiliges Chat-Eingabefeld" + }, + "old_bitmoji_selfie": { + "2d": "2D Bitmoji", + "3d": "3D Bitmoji" } } }, @@ -331,19 +863,24 @@ "preview": "Vorschau", "stealth_mode": "Inkognitomodus", "auto_download_blacklist": "Blacklist für automatische Downloads", - "anti_auto_save": "Anti-Auto-Speichern" - }, - "message_context_menu_option": { - "download": "Download", - "preview": "Vorschau" + "anti_auto_save": "Anti-Auto-Speichern", + "mark_snaps_as_seen": "Snaps als gesehen markieren", + "mark_stories_as_seen_locally": "Stories als lokal gesehen markieren" }, "chat_action_menu": { "preview_button": "Vorschau", "download_button": "Download", - "delete_logged_message_button": "Gespeicherte Nachrichten löschen" + "delete_logged_message_button": "Gespeicherte Nachrichten löschen", + "convert_message": "Nachricht konvertieren" }, "opera_context_menu": { - "download": "Medien herunterladen" + "download": "Medien herunterladen", + "media_duration": "Mediendauer: {duration} ms", + "show_debug_info": "Debug-Informationen anzeigen", + "expires_at": "Läuft am {date} ab", + "created_at": "Erstellt am {date}", + "sent_at": "Gesendet am {date}", + "media_size": "Mediengröße: {size}" }, "modal_option": { "profile_info": "Profil Info", @@ -360,26 +897,22 @@ }, "profile_info": { "title": "Profil Info", - "username": "Nutzername", + "first_created_username": "Erster Benutzername", + "mutable_username": "Änderbarer Benutzername", "display_name": "Anzeigename", "added_date": "Datum hinzugefügt", - "birthday": "Geburtstag: {month} {day}" - }, - "auto_updater": { - "no_update_available": "Kein Update verfügbar!", - "dialog_title": "Neues Update verfügbar!", - "dialog_message": "Ein neues Update für SnapEnhance ist verfügbar! ({version})\n\n{body}", - "dialog_positive_button": "Herunterladen und installieren", - "dialog_negative_button": "Abbrechen", - "downloading_toast": "Update wird Heruntergeladen...", - "download_manager_notification_title": "Lade SnapEnhance APK herunter..." + "birthday": "Geburtstag: {month} {day}", + "friendship": "Freundschaft", + "add_source": "Quelle hinzufügen", + "snapchat_plus": "Snapchat Plus", + "snapchat_plus_state": { + "subscribed": "Abonniert", + "not_subscribed": "Nicht abonniert" + }, + "hidden_birthday": "Geburtstag : Versteckt" }, "chat_export": { - "select_export_format": "Exportformat auswählen", - "select_media_type": "Wähle den Medientyp für den Export aus", - "select_conversation": "Wähle eine Unterhaltung zum Exportieren aus", "dialog_negative_button": "Abbrechen", - "dialog_neutral_button": "Alle Exportieren", "dialog_positive_button": "Exportieren", "exported_to": "Exportiert zu {path}", "exporting_chats": "Chats exportieren...", @@ -388,44 +921,44 @@ "writing_output": "Ausgabe schreiben...", "finished": "Fertig! Du kannst diesen Dialog jetzt schließen.", "no_messages_found": "Keine Nachrichten gefunden!", - "exporting_message": "{conversation} wird exportiert..." + "exporting_message": "{conversation} wird exportiert...", + "exporter_dialog": { + "text_field_selection_all": "Alle", + "export_file_format_title": "Dateiformat für den Export", + "download_medias_title": "Medien herunterladen", + "amount_of_messages_title": "Anzahl der Nachrichten (für alle leer lassen)", + "message_type_filter_title": "Nachrichten nach Typ filtern", + "text_field_selection": "{amount} ausgewählt", + "select_conversations_title": "Konversationen auswählen" + } }, "button": { "ok": "OK", "positive": "Ja", "negative": "Nein", "cancel": "Abbrechen", - "open": "Öffnen" + "open": "Öffnen", + "download": "Download" }, - "download_manager_activity": { - "remove_all_title": "Entferne alle Downloads", - "remove_all_text": "Bist du sicher, dass du das tun möchtest?", - "remove_all": "Entferne Alle", - "no_downloads": "Keine Downloads", - "cancel": "Abbrechen", - "file_not_found_toast": "Datei existiert nicht!", - "category": { - "all_category": "Alle", - "pending_category": "Ausstehend", - "snap_category": "Snaps", - "story_category": "Stories", - "spotlight_category": "Spotlight" - }, - "debug_settings": "Debug Einstellungen", - "debug_settings_page": { - "clear_file_title": "Lösche {file_name}", - "clear_file_confirmation": "Sind sie sicher, dass sie {file_name} löschen möchten?", - "clear_cache_title": "Lösche den Zwischenspeicher", - "reset_all_title": "Alle Einstellungen zurücksetzen", - "reset_all_confirmation": "Bist du sicher, dass du alle Einstellungen zurücksetzen möchten?", - "success_toast": "Erfolgreich!", - "device_spoofer": "Device Spoofer" - } + "profile_picture_downloader": { + "button": "Profilbilder herunterladen", + "title": "Profilbild-Downloader", + "avatar_option": "Avatar", + "background_option": "Hintergrund" }, "download_processor": { + "attachment_type": { + "snap": "Snap", + "sticker": "Sticker", + "external_media": "Externe Medien", + "note": "Notiz", + "original_story": "Originale Story" + }, + "select_attachments_title": "Anhänge zum Herunterladen auswählen", "download_started_toast": "Download gestartet", "unsupported_content_type_toast": "Nicht unterstützter Content-Typ!", "failed_no_longer_available_toast": "Datei ist nicht mehr verfügbar", + "no_attachments_toast": "Keine Anhänge gefunden!", "already_queued_toast": "Datei wird bereits bearbeitet!", "already_downloaded_toast": "Datei wurde bereits heruntergeladen!", "saved_toast": "Gespeichert unter {path}", @@ -436,12 +969,73 @@ "failed_processing_toast": "Fehler beim Verarbeiten {error}", "failed_gallery_toast": "Speichern in der Galerie fehlgeschlagen {error}" }, - "config_activity": { - "title": "SnapEnhance Einstellungen", - "selected_text": "{count} ausgewählt", - "invalid_number_toast": "Ungültige Nummer!" + "streaks_reminder": { + "notification_title": "Flammen", + "notification_text": "Du wirst deine Flammen mit {friend} in {hoursLeft} Stunden verlieren" + }, + "content_type": { + "FAMILY_CENTER_INVITE": "Family Center einladen", + "STATUS_CONVERSATION_CAPTURE_RECORD": "Bildschirmaufnahme", + "STATUS_CALL_MISSED_VIDEO": "Verpasster Videoanruf", + "CREATIVE_TOOL_ITEM": "Kreativ-Werkzeug Element", + "STICKER": "Sticker", + "TINY_SNAP": "Winziger Snap", + "STATUS_SAVE_TO_CAMERA_ROLL": "In Camera Roll gespeichert", + "EXTERNAL_MEDIA": "Externe Medien", + "SNAP": "Snap", + "LOCATION": "Standort", + "CHAT": "Chat", + "STATUS_PLUS_GIFT": "Status Plus Geschenk", + "STATUS_COUNTDOWN": "Countdown", + "LIVE_LOCATION_SHARE": "Live-Standort teilen", + "STATUS": "Status", + "STATUS_CONVERSATION_CAPTURE_SCREENSHOT": "Screenshot", + "FAMILY_CENTER_ACCEPT": "Family Center akzeptieren", + "FAMILY_CENTER_LEAVE": "Family Center verlassen", + "STATUS_CALL_MISSED_AUDIO": "Verpasster Sprachanruf", + "NOTE": "Sprachnachricht" + }, + "suspend_location_updates": { + "switch_text": "Standortaktualisierungen aussetzen" }, - "spoof_activity": { - "title": "Spoof Einstellungen" + "better_notifications": { + "button": { + "download": "Herunterladen", + "reply": "Antwort", + "mark_as_read": "Als gelesen markieren" + }, + "stealth_mode_notice": "Kann im Stealth-Modus nicht als gelesen markiert werden" + }, + "half_swipe_notifier": { + "notification_content_group": "{friend} hat gerade halb in {group} für {duration} Sekunden geswiped", + "notification_channel_name": "Halb-Swipe", + "notification_content_dm": "{friend} hat gerade für {duration} Sekunden halb in deinen Chat geswiped" + }, + "friendship_link_type": { + "mutual": "Gegenseitig", + "deleted": "Gelöscht", + "following": "Folge Ich", + "incoming_follower": "Eingehender Follower", + "incoming": "Eingehend", + "blocked": "Blockiert", + "suggested": "Empfohlen", + "outgoing": "Ausgehend" + }, + "call_start_confirmation": { + "dialog_message": "Sind Sie sicher, dass Sie einen Anruf starten wollen?", + "dialog_title": "Anruf starten" + }, + "bulk_messaging_action": { + "choose_action_title": "Wähle eine Aktion", + "progress_status": "Verarbeite {index} von {total}", + "actions": { + "clear_conversations": "Lösche Konversationen", + "remove_friends": "Freunde entfernen" + }, + "selection_dialog_continue_button": "Weiter", + "confirmation_dialog": { + "message": "Das betrifft alle ausgewählten Freunde. Diese Aktion kann nicht rückgängig gemacht werden.", + "title": "Sind Sie sicher?" + } } -} \ No newline at end of file +} diff --git a/common/src/main/assets/lang/fi.json b/common/src/main/assets/lang/fi.json new file mode 100644 index 000000000..9ae6f83d7 --- /dev/null +++ b/common/src/main/assets/lang/fi.json @@ -0,0 +1,702 @@ +{ + "setup": { + "dialogs": { + "select_language": "Valitse Kieli", + "save_folder": "SnapEnhance vaatii tallennus oikeudet ladatakseen mediaa Snapchatista.\nValitse sijainti mihin media tallennetaan.", + "select_save_folder_button": "Valitse kansio" + }, + "mappings": { + "dialog": "Useiden Snapchat -versioiden dynaaminen tuki edellyttää kartoituksia. Kartoitukset vaaditaan että SnapEnhance toimisi oikein. Tämän ei pitäisi kestää yli 5 sekuntia.", + "generate_button": "Luo", + "generate_failure_no_snapchat": "SnapEnhance ei havainnut Snapchattia, yritä asentaa Snapchat uudelleen.", + "generate_failure": "Kartoituksien luomisessa tapahtui virhe. Yritä uudelleen.", + "generate_success": "Kartoitukset luotu onnistuneesti." + }, + "permissions": { + "dialog": "Jatkaaksesi sinun on täytettävä seuraavat vaatimukset:", + "notification_access": "Ilmoitusten Käyttöoikeus", + "battery_optimization": "Akun Optimointi", + "display_over_other_apps": "Näytä Muiden Sovellusten Päällä", + "request_button": "Pyyntö" + } + }, + "manager": { + "routes": { + "features": "Ominaisuudet", + "home": "Etusivu", + "home_settings": "Asetukset", + "home_logs": "Lokit", + "social": "Sosiaalinen", + "scripts": "Komentosarjat" + }, + "sections": { + "home": { + "logs": { + "clear_logs_button": "Tyhjennä lokit", + "export_logs_button": "Vie lokit" + } + }, + "features": { + "disabled": "Pois päältä" + }, + "social": { + "e2ee_title": "End-to-End Salaus", + "rules_title": "Säännöt", + "participants_text": "{count} osallistujaa", + "not_found": "Ei löytynyt", + "streaks_title": "Streakit", + "streaks_length_text": "Pituus: {length}", + "streaks_expiration_short": "{hours}h", + "streaks_expiration_text": "Vanhenee {eta} kuluttua", + "reminder_button": "Aseta muistutus" + } + }, + "dialogs": { + "add_friend": { + "title": "Lisää Kaveri tai Ryhmä", + "search_hint": "Etsi", + "fetch_error": "Tietojen noutaminen epäonnistui", + "category_groups": "Ryhmät", + "category_friends": "Kaverit" + } + } + }, + "rules": { + "modes": { + "blacklist": "Blacklist tila", + "whitelist": "Whitelist tila" + }, + "properties": { + "auto_download": { + "name": "Automaattinen Lataus", + "description": "Lataa automaattisesti vastaanotetun snäpin avauksen yhteydessä", + "options": { + "blacklist": "Sulje pois automaattisesta latauksesta", + "whitelist": "Automaattinen Lataus" + } + }, + "stealth": { + "name": "Haamutila", + "description": "Estää ketään tietämästä, että olet avannut heidän snäpin tai chatin", + "options": { + "blacklist": "Sulje pois haamutilasta", + "whitelist": "Haamutila" + } + }, + "auto_save": { + "name": "Automaattinen Tallennus", + "description": "Tallentaa chat-viestit katselun yhteydessä", + "options": { + "blacklist": "Sulje pois automaattinen tallentaminen", + "whitelist": "Automaattinen tallennus" + } + }, + "hide_friend_feed": { + "name": "Piilota Ystäväsyötteestä" + }, + "e2e_encryption": { + "name": "Käytä E2E-salausta" + }, + "pin_conversation": { + "name": "Kiinnitä Keskustelu" + } + } + }, + "actions": { + "clean_snapchat_cache": "Tyhjennä Snapchatin Välimuisti", + "clear_message_logger": "Tyhjennä Viestiloki", + "refresh_mappings": "Päivitä Kartoitukset", + "open_map": "Valitse sijainti kartalta", + "check_for_updates": "Tarkista päivitykset", + "export_chat_messages": "Vie Chat-viestit" + }, + "features": { + "notices": { + "unstable": "⚠ Epävakaa", + "ban_risk": "⚠ Voi aiheuttaa eston tilillesi", + "internal_behavior": "⚠ Voi rikkoa Snapchatin sisäisiä toiminnallisuuksia", + "require_native_hooks": "⚠ Tämä vaatii kokeellisen \"Native Hooks\" ominaisuuden toimiakseen" + }, + "properties": { + "downloader": { + "name": "Lataaja", + "description": "Lataa Snapchat Mediaa", + "properties": { + "save_folder": { + "name": "Tallennus Kansio", + "description": "Valitse kansio, johon kaikki media ladataan" + }, + "auto_download_sources": { + "name": "Automaattiset Latauslähteet", + "description": "Valitse lähteet, joista ladataan automaattisesti" + }, + "prevent_self_auto_download": { + "name": "Estä Automaattiset Lataukset Itseltä", + "description": "Estää omia Snappejasi latautumasta automaattisesti" + }, + "path_format": { + "name": "Polun Muoto", + "description": "Määritä tiedostopolun muoto" + }, + "allow_duplicate": { + "name": "Salli Kaksoiskappaleet", + "description": "Salli saman median lataamisen useita kertoja" + }, + "merge_overlays": { + "name": "Yhdistä Päälitasot", + "description": "Yhdistää snäpin tekstin ja median yhdeksi tiedostoksi" + }, + "force_image_format": { + "name": "Pakota Kuvan Formaatti", + "description": "Pakottaa tallentamaan kuvat valitussa formaatissa" + }, + "force_voice_note_format": { + "name": "Pakota äänimuistiinpanomuoto", + "description": "Pakottaa äänimuistiinpanot tallentamaan tietyssä muodossa" + }, + "download_profile_pictures": { + "name": "Lataa profiilikuvat", + "description": "Antaa sinun ladata profiilikuvia profiilin sivulta" + }, + "chat_download_context_menu": { + "name": "Chat-latauksen kontekstivalikko", + "description": "Voit ladata mediaa keskustelusta painamalla niitä pitkään" + }, + "ffmpeg_options": { + "name": "FFmpeg Asetukset", + "description": "Määritä FFmpeg-lisäasetukset", + "properties": { + "threads": { + "name": "Säikeet", + "description": "Määritä rinnakkaisten säikeiden määrä" + }, + "preset": { + "name": "Esiasetus", + "description": "Aseta muunnoksen nopeus" + }, + "constant_rate_factor": { + "name": "Vakionopeustekijä", + "description": "Aseta videoenkooderin vakionopeuskerroin\n 0-51 libx264: lle" + }, + "video_bitrate": { + "name": "Videon bittinopeus", + "description": "Aseta videon bittinopeus (kbps)" + }, + "audio_bitrate": { + "name": "Äänen bittinopeus", + "description": "Aseta äänen bittinopeus (kbps)" + }, + "custom_video_codec": { + "name": "Mukautettu Videokoodekki", + "description": "Aseta mukautettu videokoodekki (esim. libx264)" + }, + "custom_audio_codec": { + "name": "Mukautettu Äänikoodekki", + "description": "Aseta mukautettu äänikoodekki (esim. AAC)" + } + } + }, + "logging": { + "name": "Lokiin kirjaus", + "description": "Näytä pieni ilmoitus kun lataus on käynnissä" + } + } + }, + "user_interface": { + "name": "Ulkoasu", + "description": "Muuta Snapchatin ulkoasua ja tuntumaa", + "properties": { + "enable_app_appearance": { + "name": "Ota käyttöön sovelluksen ulkoasuasetukset", + "description": "Ottaa käyttöön piilotetun sovelluksen ulkoasuasetuksen\nEi välttämättä vaadita uudemmissa Snapchat-versioissa" + }, + "amoled_dark_mode": { + "name": "AMOLED Tumma Tila", + "description": "Ottaa käyttöön AMOLED tumman tilan\nVarmista että Snapchatin tumma tila on päällä" + }, + "friend_feed_message_preview": { + "name": "Ystäväsyötteen Viestien Esikatselu", + "description": "Näyttää esikatselun ystäväsyötteen viimeisimmistä viesteistä", + "properties": { + "amount": { + "name": "Määrä", + "description": "Esikatseltavien viestien määrä" + } + } + }, + "bootstrap_override": { + "name": "Bootstrap Ohitus", + "description": "Ohittaa käyttöliittymän bootstrap-asetukset", + "properties": { + "app_appearance": { + "name": "Sovelluksen Ulkoasu", + "description": "Asettaa pysyvän sovelluksen ulkoasun" + }, + "home_tab": { + "name": "Koti-välilehti", + "description": "Valitse välilehti mikä aukeaa Snapchatin käynnistyessä" + } + } + }, + "map_friend_nametags": { + "name": "Parannetut ystäväkartan nimilaput", + "description": "Parantaa ystävien nimilappuja Snapkartassa" + }, + "streak_expiration_info": { + "name": "Näytä Striikkien Vanhenemistiedot", + "description": "Näyttää striikkien loppumisajastin streakkilaskurin vieressä" + }, + "hide_friend_feed_entry": { + "name": "Piilota ystäväsyöte", + "description": "Piilottaa tietyn ystävän ystäväsyötteestä\n Käytä sosiaalisen median välilehteä hallitaksesi tätä ominaisuutta" + }, + "hide_streak_restore": { + "name": "Piilota streakkien palautus", + "description": "Piilottaa Palauta-painikkeen ystäväsyötteessä" + }, + "hide_story_sections": { + "name": "Piilota tarinaosio", + "description": "Piilota tietyt tarinaosiossa näkyvät käyttöliittymäelementit" + }, + "hide_ui_components": { + "name": "Piilota käyttöliittymäkomponentit", + "description": "Valitse piilotettavat käyttöliittymäkomponentit" + }, + "disable_spotlight": { + "name": "Poista Valokeila Käytöstä", + "description": "Poistaa Valokeila-sivun käytöstä" + }, + "friend_feed_menu_buttons": { + "name": "Ystävän syötteen valikkopainikkeet", + "description": "Valitse, mitkä painikkeet näytetään ystäväsyötteen valikkopalkissa" + }, + "friend_feed_menu_position": { + "name": "Ystävän syötteen sijaintiindeksi", + "description": "Ystäväsyöte-valikkokomponentin sijainti" + }, + "enable_friend_feed_menu_bar": { + "name": "Ystäväsyötteen valikkopalkki", + "description": "Ottaa käyttöön uuden ystäväsyötteen valikkopalkin" + } + } + }, + "messaging": { + "name": "Viestintä", + "description": "Muuta tapaa, jolla olet vuorovaikutuksessa ystävien kanssa", + "properties": { + "anonymous_story_viewing": { + "name": "Nimetön tarinan katselu", + "description": "Estää ketään tietämästä, että olet nähnyt heidän tarinansa" + }, + "hide_bitmoji_presence": { + "name": "Piilota Bitmojin läsnäolo", + "description": "Estää Bitmojia ponnahtamasta esiin chatissa" + }, + "hide_typing_notifications": { + "name": "Piilota Kirjoitusilmoitukset", + "description": "Estää ketään huomaamasta, että kirjoitat viestiä" + }, + "unlimited_snap_view_time": { + "name": "Rajatot Snapin Katseluaika", + "description": "Poistaa snäppien katselun aikarajan käytöstä" + }, + "disable_replay_in_ff": { + "name": "Poista uusinta käytöstä FF: ssä", + "description": "Poistaa käytöstä mahdollisuuden toistaa kaverisyötteestä pitkällä painalluksella" + }, + "message_preview_length": { + "name": "Viestin Esikatselun Pituus", + "description": "Määritä esikatseltavien viestien määrä" + }, + "prevent_message_sending": { + "name": "Estä Viestien Lähettäminen", + "description": "Estää tietyntyyppisten viestien lähettämisen" + }, + "better_notifications": { + "name": "Paremmat ilmoitukset", + "description": "Antaa enemmän tietoja vastaanotetuissa ilmoituksissa" + }, + "notification_blacklist": { + "name": "Ilmoitusten Esto Lista", + "description": "Valitse ilmoitukset joita et halua nähdä" + }, + "message_logger": { + "name": "Viestiloki", + "description": "Estää viestien poistamisen" + }, + "auto_save_messages_in_conversations": { + "name": "Automaattinen Viestien Tallennus", + "description": "Tallentaa automaattisesti jokaisen keskustelun viestin" + }, + "gallery_media_send_override": { + "name": "Gallerian Medialähetyksen Ohitus", + "description": "Väärennä median lähde lähetettäessä galleriasta" + } + } + }, + "global": { + "name": "Globaali", + "description": "Säädä Globaaleja Snapchatin Asetuksia", + "properties": { + "spoofLocation": { + "name": "Sijainti", + "description": "Väärennä sijaintisi", + "properties": { + "coordinates": { + "name": "Koordinaatit", + "description": "Aseta koordinaatit" + } + } + }, + "snapchat_plus": { + "name": "Snapchat Plus", + "description": "Ota käyttöön Snapchat Plus ominaisuudet\nJotkin palvelinpohjaiset ominaisuudet eivät välttämättä toimi" + }, + "auto_updater": { + "name": "Automaattiset Päivitykset", + "description": "Tarkistaa automaattisesti uudet päivitykset" + }, + "disable_metrics": { + "name": "Poista Analytiikka Käytöstä", + "description": "Estä analyyttisten tietojen lähettäminen Snapchatille" + }, + "block_ads": { + "name": "Estä Mainokset", + "description": "Estää mainoksia näkymästä" + }, + "bypass_video_length_restriction": { + "name": "Ohita videon pituus rajoitukset", + "description": "Yksittäinen: lähettää yhden videon\nKatkottu: katkaise videot muokkauksen jälkeen" + }, + "disable_google_play_dialogs": { + "name": "Poista Google Play -palveluiden valintaikkunat käytöstä", + "description": "Estä Google Play -palveluiden saatavuusvalintaikkunoiden näyttäminen" + }, + "disable_snap_splitting": { + "name": "Poista Snäppien katkominen käytöstä", + "description": "Estää Snäppejä jakautumasta useisiin osiin\nLähettämäsi kuvat muuttuvat videoiksi" + } + } + }, + "rules": { + "name": "Säännöt", + "description": "Hallitse yksittäisten henkilöiden automaattisia ominaisuuksia" + }, + "camera": { + "name": "Kamera", + "description": "Säädä oikeat asetukset täydellisen kuvan saamiseksi", + "properties": { + "disable_camera": { + "name": "Estä Kamera", + "description": "Estä Snapchattia käyttämästä laitteessasi olevia kameroita" + }, + "immersive_camera_preview": { + "name": "Mukaansatempaava esikatselu", + "description": "Estää Snapchatia rajaamasta kameran esikatselua.\nTämä saattaa aiheuttaa kameran välkkymistä joissakin laitteissa" + }, + "override_preview_resolution": { + "name": "Ohita Esikatselun Resoluutio", + "description": "Ohittaa kameran esikatselun resoluution" + }, + "override_picture_resolution": { + "name": "Ohita Kuvan Resoluutio", + "description": "Ohittaa kuvan resoluution" + }, + "custom_frame_rate": { + "name": "Mukautettu Kuvataajuus", + "description": "Ohittaa kameran kuvataajuuden" + }, + "force_camera_source_encoding": { + "name": "Pakota kameran lähdekoodaus", + "description": "Pakottaa kameran lähdekoodauksen" + } + } + }, + "streaks_reminder": { + "name": "Striikkien muistutukset", + "description": "Muistuttaa sinua loppuvista striikeistä", + "properties": { + "interval": { + "name": "Aikaväli", + "description": "Kuinka usein sinua muistutetaan (tunneissa)" + }, + "remaining_hours": { + "name": "Jäljellä Oleva Aika", + "description": "Jäljellä oleva aika ennen kuin muistutus näytetään" + }, + "group_notifications": { + "name": "Ryhmä ilmoitukset", + "description": "Näytä ilmoitukset yhdessä ryhmässä" + } + } + }, + "experimental": { + "name": "Kokeelliset", + "description": "Kokeelliset ominaisuudet", + "properties": { + "native_hooks": { + "name": "Native Hooks", + "description": "Vaarallinen ominaisuus joka kytkeytyy Snapchatin alkuperäiseen koodiin", + "properties": { + "disable_bitmoji": { + "name": "Poista Bitmoji Käytöstä", + "description": "Poista kavereiden profiili Bitmojit käytöstä" + } + } + }, + "spoof": { + "name": "Väärennä", + "description": "Väärennä erinäisiä tietoja sinusta" + }, + "app_passcode": { + "name": "Sovelluksen Pääsykoodi", + "description": "Aseta pääsykoodi sovelluksen lukitsemiseksi" + }, + "app_lock_on_resume": { + "name": "Sovelluksen Lukitus Jatkossa", + "description": "Lukitsee sovelluksen, kun se avataan uudelleen" + }, + "infinite_story_boost": { + "name": "Loputon Tarinan Tehostus", + "description": "Ohita Story Boost Limit -viive" + }, + "meo_passcode_bypass": { + "name": "Vain Omille Silmille- Tunnuskoodin Ohitus", + "description": "Ohittaa salasanan \"Vain omille silmille\" osioon\nTämä toimii vain, jos salasana on syötetty oikein aiemmin" + }, + "unlimited_multi_snap": { + "name": "Rajaton Multi Snap", + "description": "Voit ottaa ja lähettää Multi Snapilla rajattoman määrän mediaa" + }, + "no_friend_score_delay": { + "name": "Ei Snäppi-Score Viivettä", + "description": "Poistaa viiveen katsottaessa Snäppi-scorea" + }, + "e2ee": { + "name": "End-To-End Salaus", + "description": "Salaa viestisi AES: llä jaetun salaisen avaimen avulla\n Muista tallentaa avaimesi turvalliseen paikkaan!", + "properties": { + "encrypted_message_indicator": { + "name": "Salatun viestin ilmaisin", + "description": "Lisää 🔒 emoji salattujen viestien viereen" + }, + "force_message_encryption": { + "name": "Pakota Viestien Salaus", + "description": "Estää salattujen viestien lähettämisen ihmisille, joilla ei ole E2E-salausta käytössä vain kun useita keskusteluja on valittu" + } + } + }, + "add_friend_source_spoof": { + "name": "Lisää ystävälähteen huijaus", + "description": "Väärentää mistä kaveripyyntö on peräisin" + }, + "hidden_snapchat_plus_features": { + "name": "Piilotetut Snapchat Plus -ominaisuudet", + "description": "Ottaa käyttöön julkaisemattomat/beta Snapchat Plus -ominaisuudet\nEi ehkä toimi vanhemmissa Snapchat-versioissa" + } + } + }, + "scripting": { + "name": "Komentojono", + "description": "Suorita mukautettuja komentosarjoja laajentaaksesi SnapEnhancea", + "properties": { + "developer_mode": { + "name": "Kehittäjätila", + "description": "Näyttää virheenkorjaustiedot Snapchatin käyttöliittymässä" + }, + "module_folder": { + "name": "Moduulin kansio", + "description": "Kansio, jossa komentosarjat sijaitsevat" + } + } + } + }, + "options": { + "app_appearance": { + "always_light": "Aina Vaalea", + "always_dark": "Aina Tumma" + }, + "better_notifications": { + "reply_button": "Lisää \"Vastaa\" -painike", + "download_button": "Lisää latauspainike", + "group": "Ryhmäilmoitukset" + }, + "friend_feed_menu_buttons": { + "auto_download": "⬇️ Automaattinen Lataus", + "auto_save": "💬 Automaattinen Viestien Tallennus", + "stealth": "👻 Haamutila", + "conversation_info": "👤 Keskustelun Tiedot", + "e2e_encryption": "🔒 Käytä E2E-salausta" + }, + "path_format": { + "create_author_folder": "Luo kansio jokaiselle kirjoittajalle", + "create_source_folder": "Luo oma kansio jokaiselle median lähdetyypille", + "append_hash": "Lisää tiedoston nimeen yksilöllinen tiiviste", + "append_source": "Lisää median lähde tiedoston nimeen", + "append_username": "Lisää käyttäjänimi tiedoston nimeen", + "append_date_time": "Lisää päivämäärä ja aika tiedoston nimeen" + }, + "auto_download_sources": { + "friend_snaps": "Kaverien Snäpit", + "friend_stories": "Kaverien Tarinat", + "public_stories": "Julkiset Tarinat", + "spotlight": "Valokeila" + }, + "logging": { + "started": "Aloitettu", + "success": "Suoritettu", + "progress": "Edistyminen", + "failure": "Epäonnistui" + }, + "notifications": { + "chat_screenshot": "Kuvakaappaus", + "chat_screen_record": "Näytön Tallennus", + "snap_replay": "Snapin Uudelleenkatselu", + "camera_roll_save": "Tallennettu kameranrullaan", + "chat": "Chatti", + "chat_reply": "Chatin Vastaus", + "snap": "Snap", + "typing": "Kirjoittaa", + "stories": "Tarinat", + "chat_reaction": "DM Reaktio", + "group_chat_reaction": "Ryhmän Reaktio", + "initiate_audio": "Saapuva Äänipuhelu", + "abandon_audio": "Vastaamaton Äänipuhelu", + "initiate_video": "Saapuva Videopuhelu", + "abandon_video": "Vastaamaton Videopuhelu" + }, + "gallery_media_send_override": { + "ORIGINAL": "Alkuperäinen", + "NOTE": "Ääniviesti", + "SNAP": "Snap", + "SAVABLE_SNAP": "Tallennettava Snap" + }, + "hide_ui_components": { + "hide_profile_call_buttons": "Poista Profiilin Soittopainikkeet", + "hide_chat_call_buttons": "Poista Chatin Puhelupainikkeet", + "hide_live_location_share_button": "Poista Sijainnin Jakopainike", + "hide_stickers_button": "Poista Tarrat -painike", + "hide_voice_record_button": "Poista Äänityspainike" + }, + "hide_story_sections": { + "hide_friend_suggestions": "Piilota ystäväehdotukset", + "hide_friends": "Piilota ystävät-osio", + "hide_suggested": "Piilota ehdotetut osio", + "hide_for_you": "Piilota sinulle -osio" + }, + "home_tab": { + "map": "Kartta", + "chat": "Chatti", + "camera": "Kamera", + "discover": "Tutustu", + "spotlight": "Valokeila" + }, + "add_friend_source_spoof": { + "added_by_username": "Käyttäjänimellä", + "added_by_mention": "Maininnasta", + "added_by_group_chat": "Ryhmäkeskustelusta", + "added_by_qr_code": "QR-koodilla", + "added_by_community": "Yhteisöstä" + }, + "bypass_video_length_restriction": { + "single": "Yksittäinen media", + "split": "Jaettu media" + } + } + }, + "friend_menu_option": { + "preview": "Esikatselu", + "stealth_mode": "Haamutila", + "auto_download_blacklist": "Automaattisen latauksen esto lista", + "anti_auto_save": "Automaattisen tallennuksen esto" + }, + "chat_action_menu": { + "preview_button": "Esikatselu", + "download_button": "Lataa", + "delete_logged_message_button": "Poista kirjattu viesti" + }, + "opera_context_menu": { + "download": "Lataa Media" + }, + "modal_option": { + "profile_info": "Profiilin Tiedot", + "close": "Sulje" + }, + "gallery_media_send_override": { + "multiple_media_toast": "Voit lähettää vain yhden median kerrallaan" + }, + "conversation_preview": { + "streak_expiration": "loppuu {day} päivän {hour} tunnin {minute} minuutin kuluttua", + "total_messages": "Lähetetyt/vastaanotetut viestit yhteensä: {count}", + "title": "Esikatselu", + "unknown_user": "Tuntematon käyttäjä" + }, + "profile_info": { + "title": "Profiilin tiedot", + "first_created_username": "Ensimmäinen Käyttäjätunnus", + "mutable_username": "Muutettava Käyttäjätunnus", + "display_name": "Näyttönimi", + "added_date": "Lisätty Päivämäärä", + "birthday": "Syntymäpäivä: {month} {day}", + "friendship": "Ystävyys", + "add_source": "Lisää lähde", + "snapchat_plus": "Snapchat Plus", + "snapchat_plus_state": { + "subscribed": "Tilattu", + "not_subscribed": "Ei Tilattu" + } + }, + "chat_export": { + "dialog_negative_button": "Peruuta", + "dialog_positive_button": "Vie", + "exported_to": "Viety {path}", + "exporting_chats": "Viedään Keskusteluja...", + "processing_chats": "Käsitellään {amount} keskustelu(a)...", + "export_fail": "Ei voitu viedä keskustelua {conversation}", + "writing_output": "Tulostetta kirjoitetaan...", + "finished": "Valmis! Voit nyt sulkea tämän ikkunan.", + "no_messages_found": "Viestejä ei löytynyt!", + "exporting_message": "Viedään {conversation}..." + }, + "button": { + "ok": "OK", + "positive": "Kyllä", + "negative": "Ei", + "cancel": "Peruuta", + "open": "Avaa", + "download": "Lataa" + }, + "profile_picture_downloader": { + "button": "Lataa Profiilikuva", + "title": "Profiilikuvan Lataaja", + "avatar_option": "Hahmo", + "background_option": "Tausta" + }, + "download_processor": { + "attachment_type": { + "snap": "Snap", + "sticker": "Tarra", + "external_media": "Ulkoinen Media", + "note": "Merkintä", + "original_story": "Alkuperäinen Tarina" + }, + "select_attachments_title": "Valitse ladattavat liitteet", + "download_started_toast": "Lataus aloitettu", + "unsupported_content_type_toast": "Sisältötyyppiä ei tueta!", + "failed_no_longer_available_toast": "Media ei ole enää saatavilla", + "no_attachments_toast": "Liitteitä ei löytynyt!", + "already_queued_toast": "Media on jo jonossa!", + "already_downloaded_toast": "Media on jo ladattu!", + "saved_toast": "Tallennettu sijaintiin {path}", + "download_toast": "Ladataan {path}...", + "processing_toast": "Käsitellään {path}...", + "failed_generic_toast": "Lataus epäonnistui", + "failed_to_create_preview_toast": "Esikatselun luominen epäonnistui", + "failed_processing_toast": "Käsittely epäonnistui {error}", + "failed_gallery_toast": "Tallennus galleriaan epäonnistui {error}" + }, + "streaks_reminder": { + "notification_title": "Striikit", + "notification_text": "Menetät Striikit {friend}: n kanssa {hoursLeft} tunnin kuluttua" + } +} diff --git a/common/src/main/assets/lang/fr_FR.json b/common/src/main/assets/lang/fr_FR.json index 11b0a5cd6..ec3e19436 100644 --- a/common/src/main/assets/lang/fr_FR.json +++ b/common/src/main/assets/lang/fr_FR.json @@ -1,34 +1,32 @@ { "setup": { "dialogs": { - "select_language": "Choisissez votre langue", - "save_folder": "SnapEnhance requiert des permissions de stockage pour télécharger et sauvegarder les médias de Snapchat\nChoisissez un emplacement de sauvegarde pour les médias téléchargés", - "select_save_folder_button": "Choisir le dossier" + "select_language": "Choisir la langue", + "select_save_folder_button": "Sélectionner un dossier" }, "mappings": { - "dialog": "Pour prendre en charge de nombreuses versions de Snapchat de manière dynamique, des mappages sont nécessaires pour que SnapEnhance fonctionne correctement, ce qui ne devrait pas prendre plus de 5 secondes.", + "dialog": "Pour prendre en charge dynamiquement une large étendue de versions de Snapchat, les mappings sont nécessaires pour que SnapEnhance fonctionne correctement, cela ne devrait pas prendre plus de 5 secondes.", "generate_button": "Générer", - "generate_failure_no_snapchat": "SnapEnhance n'a pas pu détecter Snapchat, veuillez essayer de réinstaller Snapchat.", + "generate_failure_no_snapchat": "SnapEnhance n'a pas pu détecter Snapchat, essayez de réinstaller Snapchat.", "generate_failure": "Une erreur s'est produite lors de la génération des mappings, veuillez réessayer.", - "generate_success": "Les mappages ont été générés avec succès." + "generate_success": "Mappings générés avec succès." }, "permissions": { "dialog": "Pour continuer, vous devez remplir les conditions suivantes :", "notification_access": "Accès aux notifications", - "battery_optimization": "Désactiver l'optimisation de la batterie", - "request_button": "Autoriser" + "battery_optimization": "Optimisation de la batterie", + "display_over_other_apps": "Superposer aux autres applis", + "request_button": "Autorisation" } }, - "manager": { "routes": { - "downloads": "Téléchargements", - "features": "Fonctionnalités", + "features": "Options", "home": "Accueil", - "home_debug": "Débug", + "home_settings": "Paramètres", "home_logs": "Logs", - "social": "Social", - "plugins": "Plugins" + "social": "Réseaux sociaux", + "scripts": "Scripts" }, "sections": { "home": { @@ -37,34 +35,31 @@ "export_logs_button": "Exporter les logs" } }, - "downloads": { - "empty_download_list": "(vide)" - }, "features": { "disabled": "Désactivé" }, "social": { + "e2ee_title": "Chiffrement de bout-en-bout", "rules_title": "Règles", "participants_text": "{count} participants", "not_found": "Introuvable", - "streaks_title": "Flammes", - "streaks_length_text": "Durée {length}", + "streaks_title": "Rappels des flammes", + "streaks_length_text": "Longueur : {length}", "streaks_expiration_short": "{hours}h", "streaks_expiration_text": "Expire dans {eta}", - "reminder_button": "Activer les rappels" + "reminder_button": "Définir le rappel" } }, "dialogs": { "add_friend": { - "title": "Ajouter un ami ou un groupe", + "title": "Ajouter des amis ou un groupe", "search_hint": "Rechercher", - "fetch_error": "Impossible de récupérer la liste de données", + "fetch_error": "Échec de la récupération des données", "category_groups": "Groupes", "category_friends": "Amis" } } }, - "rules": { "modes": { "blacklist": "Mode liste noire", @@ -73,612 +68,579 @@ "properties": { "auto_download": { "name": "Téléchargement automatique", - "description": "Télécharge automatiquement les médias de Snapchat lorsqu'ils sont vus", + "description": "Télécharger automatiquement les Snaps lors de leur visionnage", "options": { "blacklist": "Exclure du téléchargement automatique", "whitelist": "Téléchargement automatique" } }, "stealth": { - "name": "Mode furtif", - "description": "Empêche quiconque de savoir que vous avez ouvert leurs Snaps/Chats et conversations", + "name": "Mode incognito", + "description": "Empêche quiconque de savoir que vous avez ouvert ses Snaps/Chats", "options": { - "blacklist": "Exclure du mode furtif", - "whitelist": "Mode furtif" + "blacklist": "Exclure du mode incognito", + "whitelist": "Mode incognito" } }, "auto_save": { "name": "Sauvegarde automatique", - "description": "Sauvegarde automatiquement les messages de chat lorsqu'ils sont vus", + "description": "Enregistre les messages lors de leur visionnage", "options": { "blacklist": "Exclure de la sauvegarde automatique", "whitelist": "Sauvegarde automatique" } }, - "hide_chat_feed": { - "name": "Masquer du flux de chat" + "hide_friend_feed": { + "name": "Masquer du flux d'amis" + }, + "e2e_encryption": { + "name": "Utilisatrice le chiffrement E2E" + }, + "pin_conversation": { + "name": "Épingler la conversation" } } }, - "actions": { - "clean_snapchat_cache": "Nettoyer le cache de Snapchat", - "clear_message_logger": "Effacer le journal des messages", - "refresh_mappings": "Rafraîchir les mappages", - "open_map": "Ouvrir la carte", - "export_chat_messages": "Exporter les messages de chat" + "clean_snapchat_cache": "Effacer le cache Snapchat", + "clear_message_logger": "Effacer les journeaux des messages", + "refresh_mappings": "Actualiser les mappages", + "open_map": "Choisir un emplacement sur la carte", + "check_for_updates": "Vérifier les mises à jour", + "export_chat_messages": "Exporter les messages du chat" }, - "features": { "notices": { - "unstable": "\u26A0 Instable", - "ban_risk": "\u26A0 Cette fonctionnalité peut entraîner un bannissement", - "internal_behavior": "\u26A0 Cette fonctionnalité peut causer des comportements internes inattendus" + "unstable": "⚠️ Instable", + "ban_risk": "⚠️ Cette fonctionnalité pourrait causer des bannissements", + "internal_behavior": "⚠️ Cela peut casser le comportement interne de Snapchat", + "require_native_hooks": "⚠️ Cette fonctionnalité nécessite des accrochages natifs expérimentaux pour fonctionner correctement" }, "properties": { "downloader": { - "name": "Téléchargements", - "description": "Télécharger les médias de Snapchat", + "name": "Téléchargeur", + "description": "Télécharger médias de Snapchat", "properties": { "save_folder": { - "name": "Dossier de sauvegarde", - "description": "Sélectionnes le dossier dans lequel tous les médias doivent être téléchargés" + "name": "Dossier d'enregistrement", + "description": "Sélectionnez le répertoire dans lequel tous les médias doivent être téléchargés" }, "auto_download_sources": { - "name": "Sources de téléchargement automatique", - "description": "Sélectionnez les sources à télécharger automatiquement" + "name": "Sources de téléchargements automatiques", + "description": "Sélectionner les sources pour lesquelles les téléchargements seront automatiques" }, "prevent_self_auto_download": { - "name": "Empêcher l'auto téléchargement de soi-même", - "description": "Empêche le téléchargement automatique de vos propres medias" + "name": "Empêcher l'automatisation du téléchargement", + "description": "Empêcher vos propres Snaps d'êtres automatiquement téléchargement" }, "path_format": { - "name": "Format du chemin du fichier", - "description": "Spécifie le format du chemin du fichier" + "name": "Format du chemin d'accès", + "description": "Spécifier le format de l'emplacement de fichier" }, "allow_duplicate": { "name": "Autoriser les doublons", - "description": "Autorise le même média à être téléchargé plusieurs fois" + "description": "Permet au même média d'être téléchargé plusieurs fois" }, "merge_overlays": { - "name": "Fusionner les snaps contenant des superpositions", - "description": "Combine le texte et le média d'un Snap dans un seul fichier" + "name": "Fusionner les superpositions", + "description": "Combine le texte et le média d'un Snap en un seul fichier" }, "force_image_format": { "name": "Forcer le format d'image", - "description": "Force les images à être enregistrées dans un format spécifié" + "description": "Force l'enregistrement des images dans un format spécifié" }, "force_voice_note_format": { - "name": "Forcer le format des vocaux", - "description": "Force les vocaux à être enregistrés dans un format spécifié" + "name": "Forcer le format de la note vocale", + "description": "Forcer l'enregistrement des notes vocales dans un format spécifié" }, "download_profile_pictures": { "name": "Télécharger les photos de profil", - "description": "Permets de télécharger les photos de profil depuis la page de profil" + "description": "Vous permet de télécharger les photos du profil depuis la page de profil" }, "chat_download_context_menu": { - "name": "Menu contextuel de téléchargement de chat", - "description": "Permets de télécharger des médias d'une conversation en effectuant un appui long sur le message" + "name": "Menu contextuel de téléchargement du chat", + "description": "Vous permet de télécharger des médias d'une conversation en appuyant longuement sur eux" }, "ffmpeg_options": { - "name": "Options de FFmpeg", - "description": "Spécifie les options de FFmpeg", + "name": "Options FFmpeg", + "description": "Spécifier des options supplémentaires pour FFmpeg", "properties": { "threads": { - "name": "Threads", - "description": "Le nombre de threads à utiliser pour le traitement" + "name": "Fil de discussions", + "description": "Le nombre de fils de discussions à utiliser" }, "preset": { - "name": "Pré-réglages", - "description": "Défini la vitesse de traitement" + "name": "Préréglages", + "description": "Définir la vitesse de conversion" }, "constant_rate_factor": { "name": "Facteur de taux constant", - "description": "Défini le facteur de taux constant pour l'encodeur vidéo\nDe 0 à 51 pour libx264" + "description": "Définir le facteur de débit constant pour l'encodeur vidéo\nde 0 à 51 pour libx264" }, "video_bitrate": { "name": "Débit vidéo", - "description": "Défini le débit vidéo (en kbps)" + "description": "Définir le débit vidéo (kbps)" }, "audio_bitrate": { "name": "Débit audio", - "description": "Défini le débit audio (en kbps)" + "description": "Définir le débit audio (kbps)" }, "custom_video_codec": { - "name": "Codec vidéo personnalisé", - "description": "Défini un codec vidéo personnalisé (par exemple libx264)" + "name": "Codec audio personnalisé", + "description": "Définir un Codec Vidéo personnalisé tel que (libx264)" }, "custom_audio_codec": { "name": "Codec audio personnalisé", - "description": "Défini un codec audio personnalisé (par exemple aac)" + "description": "Définir un Codec Audio personnalisé tel que (AAC)" } } }, "logging": { - "name": "Journalisation", - "description": "Affiche des indications éphémères lorsque les médias sont téléchargés" + "name": "Enregistrement", + "description": "Afficher une bulle de notification lorsque le média est en téléchargement" } } }, "user_interface": { "name": "Interface utilisateur", - "description": "Change l'apparence de Snapchat", + "description": "Changer l'apparence de Snapchat", "properties": { "enable_app_appearance": { - "name": "Activer l'apparence de l'application", - "description": "Active le paramètre d'apparence de l'application caché\nPeut ne pas être nécessaire sur les nouvelles versions de Snapchat" + "name": "Activer les paramètres d'apparence de l'appli", + "description": "Active le paramètre d’apparence caché de l’application\nPeut ne pas être nécessaire sur les nouvelles versions de Snapchat" }, "amoled_dark_mode": { "name": "Mode sombre AMOLED", - "description": "Active le mode sombre AMOLED\nAssurez-vous que le mode sombre de Snapchat est activé" + "description": "Activer le mode sombre pour les écrans AMOLED\nAssurez-vous d'avoir bien activé le mode sombre de Snapchat" + }, + "friend_feed_message_preview": { + "name": "Aperçu du message du flux d'amis", + "description": "Affiche un aperçu des derniers messages du flux d'ami", + "properties": { + "amount": { + "name": "Quantité", + "description": "Le nombre de messages à prévisualiser" + } + } + }, + "bootstrap_override": { + "name": "Remplacement de l'interface d'utilisateur", + "description": "Contourne les paramètres de démarrage de l'interface utilisateur", + "properties": { + "app_appearance": { + "name": "Apparance de l'application", + "description": "Définit une apparence persistante de l'application" + }, + "home_tab": { + "name": "Onglet d'accueil", + "description": "Remplacement de l'onglet à l'ouverture" + } + } }, "map_friend_nametags": { - "name": "Amélioration des nametags d'amis sur la carte", - "description": "Améliore les nametags des amis sur la Snapmap" + "name": "Améliorations des nametags d'amis sur la Carte Snap", + "description": "Améliore-les nametags des amis sur la Carte Snap" }, "streak_expiration_info": { - "name": "Afficher les informations d'expiration des flammes", - "description": "Affiche un chronomètre d'expiration des flammes à côté du compteur de flammes" + "name": "Infos sur l'expiration des flammes", + "description": "Affiche un compteur d'expiration de flamme à côté du compteur de flamme" + }, + "hide_streak_restore": { + "name": "Masque la restauration de Snapflamme", + "description": "Masque le bouton de restauration" }, "hide_story_sections": { - "name": "Masquer les sections des stories", - "description": "Masque certains éléments de l'interface utilisateur affichés dans la section des stories" + "name": "Masque la section Stories", + "description": "Masquer certains éléments visuels affichés dans la section des stories" }, "hide_ui_components": { - "name": "Masquer les composants de l'interface utilisateur", - "description": "Sélectionne les composants de l'interface utilisateur à masquer" - }, - "2d_bitmoji_selfie": { - "name": "Selfie Bitmoji 2D", - "description": "Restaure les selfies Bitmoji 2D des anciennes versions de Snapchat\nVous devrez peut-être nettoyer le cache de Snapchat pour que cela prenne effet" + "name": "Masque les composants de l'interface utilisateur", + "description": "Sélectionner quels éléments de l'interface est à masqué" }, "disable_spotlight": { - "name": "Désactiver Spotlight", + "name": "Désactive la section Spotlight", "description": "Désactive la page Spotlight" }, - "startup_tab": { - "name": "Onglet de démarrage", - "description": "Change l'onglet qui s'ouvre au démarrage" - }, - "story_viewer_override": { - "name": "Remplacement du visionneur de story", - "description": "Active certaines fonctionnalités que Snapchat a cachées" - }, "friend_feed_menu_buttons": { - "name": "Boutons du menu contextuel des amis", - "description": "Séléctionne les boutons à afficher dans le menu contextuel des amis" + "name": "Boutons dans le chat d'amis", + "description": "Sélectionner les boutons à afficher dans la barre de menu du fil d'amis" }, "friend_feed_menu_position": { - "name": "Position du menu contextuel des amis", - "description": "La position du menu contextuel des amis" + "name": "Position des boutons du chat d'ami", + "description": "La position des boutons du menu du fil des amis" + }, + "enable_friend_feed_menu_bar": { + "description": "Active la nouvelle barre de menu de flux d'amis" } } }, "messaging": { - "name": "Messagerie", - "description": "Change la façon dont vous interagissez avec vos amis", + "name": "Messages", + "description": "Changez la façon dont vous interagissez avec vos amis", "properties": { "anonymous_story_viewing": { - "name": "Visionnage anonyme des stories", - "description": "Empêche quiconque de savoir que vous avez vu leur story" + "name": "Anonymiser le visionnage des stories", + "description": "Empêcher n'importe qui de savoir que vous avez vu leur story" }, "hide_bitmoji_presence": { - "name": "Masquer la présence Bitmoji", - "description": "Empêche votre Bitmoji de s'afficher pendant que vous êtes dans une conversation" + "name": "Cacher le Bitmoji dans la conversation", + "description": "Empêche votre Bitmoji d'apparaître dans le chat" }, "hide_typing_notifications": { - "name": "Masquer les notifications de saisie", - "description": "Empêche quiconque de savoir que vous êtes en train de taper un message" + "name": "Masquer la notification \"En train d'écrire\"", + "description": "Empêcher n'importe qui de savoir que vous avez lu leurs messages" }, "unlimited_snap_view_time": { - "name": "Temps de visionnage illimité", - "description": "Supprime la limite de temps pour visionner les Snaps" + "name": "Temps de visionnage des Snaps illimités", + "description": "Supprime la limite de temps pour la visualisation des Snaps" }, "disable_replay_in_ff": { - "name": "Désactiver le revisionnage dans le flux d'amis", - "description": "Désactive la possibilité de revisionner avec un appui long depuis le flux d'amis" + "name": "Désactiver la relecture en FF", + "description": "Désactive la possibilité de rejouer avec un appui long du Flux d'Ami" + }, + "message_preview_length": { + "name": "Longueur de la prévisualisation du message", + "description": "Spécifier le nombre de messages à prévisualiser" }, "prevent_message_sending": { - "name": "Empêcher l'envoi de messages", - "description": "Empêche l'envoi de certains types de messages" + "name": "Empêcher l'envoi de message", + "description": "Empêche l'envoi de certains types de message" }, "better_notifications": { "name": "Notifications améliorées", - "description": "Ajoute plus d'informations dans les notifications reçues" + "description": "Ajouter plus d'informations dans les notifications reçues" }, "notification_blacklist": { "name": "Liste noire des notifications", - "description": "Sélectionnez les notifications qui doivent être bloquées" + "description": "Sélectionnez les notifications qui devraient être bloquées" }, "message_logger": { - "name": "Journal des messages", - "description": "Empêche les messages d'être supprimés par l'envoyeur" + "name": "Journalisation des messages", + "description": "Empêcher l'effacement des messages" }, "auto_save_messages_in_conversations": { - "name": "Sauvegarde automatique des messages", - "description": "Sauvegarde automatiquement chaque message dans les conversations" + "name": "Enregistrement automatique des messages", + "description": "Enregistre automatiquement tous les messages des conversations" }, "gallery_media_send_override": { - "name": "Remplacement de l'envoi de médias de la galerie", - "description": "Modifie le type de média lors de l'envoi depuis la galerie" - }, - "message_preview_length": { - "name": "Longueur de l'aperçu des messages", - "description": "Spécifie le nombre de messages à prévisualiser" + "name": "Remplacement de l'envoi des médias de la galerie", + "description": "Falsifie la source du média lors de l'envoi depuis la Galerie" } } }, "global": { "name": "Global", - "description": "Change les paramètres globaux de Snapchat", + "description": "Modifier les paramètres globaux de Snapchat", "properties": { + "spoofLocation": { + "name": "Localisation", + "description": "Falsifier votre localisation", + "properties": { + "coordinates": { + "name": "Coordonnées", + "description": "Définir les coordonnées" + } + } + }, "snapchat_plus": { "name": "Snapchat Plus", - "description": "Active les fonctionnalités de Snapchat Plus\nCertaines fonctionnalités côté serveur peuvent ne pas fonctionner" + "description": "Activer les fonctionnalités de Snapchat Plus\nCertaines fonctionnalités côté serveur peuvent ne pas fonctionner" + }, + "auto_updater": { + "name": "Mise à jour automatique", + "description": "Vérifier automatiquement les mises à jour" }, "disable_metrics": { "name": "Désactiver les métriques", - "description": "Bloque l'envoi de données analytiques spécifiques à Snapchat" + "description": "Bloque l'envoi de données analytiques spécifiques vers Snapchat" }, "block_ads": { "name": "Bloquer les publicités", "description": "Empêche l'affichage des publicités" }, - "disable_video_length_restrictions": { - "name": "Désactiver les restrictions de durée des vidéos", - "description": "Désactive la restriction de durée maximale des vidéos de Snapchat" + "bypass_video_length_restriction": { + "name": "Contourner la restriction de longueur vidéo", + "description": "Unique : envoie une seule vidéo\nDécoupé : découper les vidéos après l'édition" }, "disable_google_play_dialogs": { - "name": "Désactiver les pop-up Google Play", - "description": "Empêche l'affichage des pop-up de disponibilité des services Google Play" - }, - "force_media_source_quality": { - "name": "Forcer la qualité originale des médias", - "description": "Force la qualité des médias de Snapchat à la valeur spécifiée" + "name": "Désactiver les avertissements des services Google Play", + "description": "Empêcher les services Google Play d'afficher des boites de dialogues" }, "disable_snap_splitting": { - "name": "Désactiver la division des Snaps", - "description": "Empêche les Snaps d'être divisés en plusieurs parties\nLes photos que vous envoyez se transformeront en vidéos" + "name": "Désactiver le fractionnement des Snaps", + "description": "Empêche les Snaps d'être divisés en plusieurs parties\nLes images que vous envoyez seront transformées en vidéos" } } }, "rules": { "name": "Règles", - "description": "Gérez les fonctionnalités automatiques pour chaque personne" + "description": "Gérer les fonctionnalités automatiques pour les personnes individuelles" }, "camera": { "name": "Caméra", - "description": "Ajustez les bons paramètres pour le snap parfait", + "description": "Ajuster les bons réglages pour le Snap parfait", "properties": { "disable_camera": { "name": "Désactiver la caméra", "description": "Empêche Snapchat d'utiliser les caméras disponibles sur votre appareil" }, "immersive_camera_preview": { - "name": "Aperçu immersif de la caméra", - "description": "Empêche Snapchat de recadrer l'aperçu de la caméra\nCela peut provoquer des scintillements de la caméra sur certains appareils" + "name": "Aperçu de la caméra immersif", + "description": "Empêche Snapchat de recadrer l'aperçu de la caméra\nCela peut faire clignoter la caméra sur certains appareils" }, "override_preview_resolution": { - "name": "Remplacement de la résolution de l'aperçu de la caméra", + "name": "Remplacer la résolution de prévisualisation", "description": "Remplace la résolution de l'aperçu de la caméra" }, "override_picture_resolution": { - "name": "Remplacement de la résolution de l'image", - "description": "Remplace la résolution de l'image" + "name": "Remplace la résolution de la photo", + "description": "Remplace la résolution de la photo" }, "custom_frame_rate": { - "name": "Taux d'image par seconde personnalisé", - "description": "Remplace le taux d'image par seconde de la caméra" + "name": "Fréquence d'images personnalisée", + "description": "Outrepasse la fréquence d’images de la caméra" }, "force_camera_source_encoding": { - "name": "Forcer l'encodage de la source de la caméra", - "description": "Force l'encodage de la source de la caméra" + "name": "Forcer l'encodage source de la caméra", + "description": "Forcer l'encodage source de la caméra" } } }, "streaks_reminder": { - "name": "Rappels des flammes", - "description": "Vous rappelle périodiquement vos flammes", + "name": "Rappels de série", + "description": "Vous notifie périodiquement de vos séries", "properties": { "interval": { - "name": "Intervalle", + "name": "Fréquence", "description": "L'intervalle entre chaque rappel (heures)" }, "remaining_hours": { - "name": "Heures restantes", - "description": "Le temps restant avant que la notification ne s'affiche" + "name": "Temps restant", + "description": "Le temps restant avant que la notification soit affichée" }, "group_notifications": { - "name": "Regrouper les notifications", - "description": "Regroupe les notifications en une seule" + "name": "Notifications de groupe", + "description": "Grouper les notifications en une seule" } } }, "experimental": { "name": "Expérimental", - "description": "Active les fonctionnalités expérimentales", + "description": "Fonctionnalités expérimentales", "properties": { "native_hooks": { "name": "Hooks natifs", - "description": "Fonctionnalités non sûres qui se greffe au code natif de Snapchat", "properties": { "disable_bitmoji": { - "name": "Désactiver les Bitmoji", - "description": "Désactive les Bitmoji de profil d'amis" - }, - "fix_gallery_media_override": { - "name": "Correction de l'envoi de médias de la galerie", - "description": "Corrige divers problèmes avec la fonctionnalité d'envoi de médias de la galerie (par exemple, enregistrer les Snaps dans le chat)" + "name": "Désactiver le Bitmoji", + "description": "Désactive le Bitmoji sur le profil d'amis" } } }, "spoof": { - "name": "Spoof", - "description": "Spoof diverses informations vous concernant", - "properties": { - "location": { - "name": "Localisation", - "description": "Spoof votre localisation", - "properties": { - "location_latitude": { - "name": "Latitude", - "description": "La latitude de la localisation" - }, - "location_longitude": { - "name": "Longitude", - "description": "La longitude de la localisation" - } - } - }, - "device": { - "name": "Appareil", - "description": "Spoof diverses informations sur votre appareil", - "properties": { - "fingerprint": { - "name": "Empreinte de l'appareil", - "description": "Spoof l'empreinte de votre appareil" - }, - "android_id": { - "name": "Android ID", - "description": "Spoof l'ID Android de votre appareil" - }, - "installer_package_name": { - "name": "Nom du package de l'installateur", - "description": "Spoof le nom du package de l'installateur" - }, - "debug_flag": { - "name": "Debug Flag", - "description": "Makes Snapchat debuggable" - }, - "mock_location": { - "name": "Mock location", - "description": "Spoofs the Mock Location device state" - }, - "split_classloader": { - "name": "Split Classloader", - "description": "Spoofs splitClassloader\nRequested by org.chromium.base.JNIUtils" - } - } - } - } + "name": "Falsification", + "description": "Falsifier diverses informations vous concernant" }, "app_passcode": { - "name": "Code d'accès à l'application", - "description": "Défini un code d'accès à l'application" + "name": "Code d'accès de l'application", + "description": "Définit un mot de passe pour verrouiller l'application" }, "app_lock_on_resume": { - "name": "Verrouillage de l'application à la reprise", - "description": "Verrouille l'application lorsque vous revenez à Snapchat" + "description": "Verrouille l'application lorsqu'elle est réouverte" }, "infinite_story_boost": { - "name": "Boost infini des stories", - "description": "Contourne le délai de limite de boost de story" - }, - "meo_passcode_bypass": { - "name": "Contournement du code d'accès My Eyes Only", - "description": "Contourne le code d'accès My Eyes Only\nCela ne fonctionnera que si le code d'accès a été entré correctement auparavant" + "name": "Booster de story infini", + "description": "Contournez le délai de Boost de Story" }, "unlimited_multi_snap": { "name": "Multi Snap illimité", - "description": "Vous permet de prendre un nombre illimité de Multi Snaps" + "description": "Vous permet d'avoir un nombre illimité de multi snap" }, "no_friend_score_delay": { - "name": "Pas de délai de snap score d'ami", - "description": "Supprime le délai lors de la visualisation du snap score d'un ami" + "name": "Pas de délai du score d'amis", + "description": "Supprime le délai lors de la visualisation d'un score d'amis" + }, + "e2ee": { + "name": "Chiffrement de bout-en-bout", + "description": "Chiffre vos messages avec AES à l'aide d'une clé secrète\nAssurez-vous de sauvegarder votre clé quelque part en sécurité !", + "properties": { + "encrypted_message_indicator": { + "name": "Indicateur de message chiffré", + "description": "Ajoute un émoji 🔒 à côté des messages chiffrés" + }, + "force_message_encryption": { + "name": "Forcer le chiffrement des messages", + "description": "Empêche l'envoi de messages chiffrés aux personnes qui n'ont pas de chiffrement de bout en bout activé uniquement lorsque plusieurs conversations sont sélectionnées" + } + } }, "add_friend_source_spoof": { - "name": "Spoof de la source d'ajout d'ami", - "description": "Spoof la source d'une demande d'ami" + "description": "Falsifie la source d'une demande d'ami" + }, + "hidden_snapchat_plus_features": { + "name": "Fonctionnalités Snapchat Plus cachées", + "description": "Active les fonctionnalités non publiées/beta de Snapchat Plus\nPeut ne pas fonctionner sur les anciennes versions de Snapchat" + } + } + }, + "scripting": { + "name": "Scripting", + "description": "Exécuter des scripts personnalisés pour étendre SnapEnhance", + "properties": { + "developer_mode": { + "name": "Mode Développeur", + "description": "Affiche les informations de débogage sur l'interface utilisateur de Snapchat" + }, + "module_folder": { + "name": "Dossier des modules", + "description": "Le dossier où se trouvent les scripts" } } } }, "options": { + "app_appearance": { + "always_light": "Toujours Clair", + "always_dark": "Toujours Sombre" + }, "better_notifications": { - "chat": "Afficher les messages de chat", - "snap": "Afficher les médias de Snap", "reply_button": "Ajouter un bouton de réponse", - "download_button": "Ajoouter un bouton de téléchargement", - "group": "Grouper les notifications" + "download_button": "Ajouter un boutton téléchargement", + "group": "Notifications de groupe" }, "friend_feed_menu_buttons": { - "auto_download": "\u2B07\uFE0F Téléchargement automatique", - "auto_save": "\uD83D\uDCAC Sauvegarde automatique", - "stealth": "\uD83D\uDC7B Mode furtif", - "conversation_info": "\uD83D\uDC64 Informations de la conversation" + "auto_download": "⬇️ Téléchargement automatique", + "auto_save": "💬 Enregistrement automatique des messages", + "stealth": "👻 Mode incognito", + "conversation_info": "👤 Infos de la Conversation", + "e2e_encryption": "🔒 Utiliser le chiffrement de bout en bout" }, "path_format": { - "create_author_folder": "Créer un dossier pour chaque auteur", - "create_source_folder": "Créer un dossier pour chaque type de source de média", - "append_hash": "Ajouter un hash unique au nom du fichier", + "create_author_folder": "Créer un dossier pour chaque utilisateur", + "create_source_folder": "Créer un dossier pour chaque type de média", + "append_hash": "Ajouter une empreinte unique au nom du fichier", "append_source": "Ajouter la source du média au nom du fichier", "append_username": "Ajouter le nom d'utilisateur au nom du fichier", - "append_date_time": "Ajouter la date et l'heure au nom du fichier" + "append_date_time": "Ajouter la date ainsi que l'heure au nom du fichier" }, "auto_download_sources": { - "friend_snaps": "Snaps d'amis", - "friend_stories": "Stories d'amis", + "friend_snaps": "Snaps des amis", + "friend_stories": "Stories des amis", "public_stories": "Stories publiques", "spotlight": "Spotlight" }, "logging": { - "started": "Au lancement", + "started": "Démarré", "success": "Succès", - "progress": "En cours", + "progress": "Progression", "failure": "Échec" }, - "auto_save_messages_in_conversations": { - "NOTE": "Messages vocaux", - "CHAT": "Messages de chat", - "EXTERNAL_MEDIA": "Médias externes", - "SNAP": "Snaps", - "STICKER": "Stickers" - }, "notifications": { - "chat_screenshot": "Capture d'écran de chat", - "chat_screen_record": "Enregistrement d'écran de chat", + "chat_screenshot": "Capture d'écran", + "chat_screen_record": "Enregistrement vidéo de l'écran", + "snap_replay": "Revisionnage du Snap", "camera_roll_save": "Sauvegarde de la pellicule", - "chat": "Chat", - "chat_reply": "Réponse de chat", + "chat": "Discussion", + "chat_reply": "Répondre", "snap": "Snap", - "typing": "Saisie", + "typing": "Saisie en cours", "stories": "Stories", + "chat_reaction": "Réaction du MP", + "group_chat_reaction": "Réaction du groupe", "initiate_audio": "Appel audio entrant", "abandon_audio": "Appel audio manqué", "initiate_video": "Appel vidéo entrant", "abandon_video": "Appel vidéo manqué" }, "gallery_media_send_override": { - "ORIGINAL": "Contenu original", - "NOTE": "Message vocal", - "SNAP": "Snap", - "SAVABLE_SNAP": "Snap sauvegardable" - }, - "hide_ui_components": { - "hide_call_buttons": "Supprimer les boutons d'appel", - "hide_cognac_button": "Supprimer le bouton Cognac", - "hide_live_location_share_button": "Supprimer le bouton de partage de la position en direct", - "hide_stickers_button": "Supprimer le bouton des stickers", - "hide_voice_record_button": "Supprimer le bouton d'enregistrement vocal" - }, - "story_viewer_override": { - "OFF": "Désactivé", - "DISCOVER_PLAYBACK_SEEKBAR": "Activer la barre de lecture dans Discover", - "VERTICAL_STORY_VIEWER": "Activer le visionneur de story vertical" - }, - "hide_story_sections": { - "hide_friend_suggestions": "Masquer la section des suggestions d'amis", - "hide_friends": "Masquer la section des amis", - "hide_following": "Masquer la section des abonnements", - "hide_for_you": "Masquer la section Pour vous" - }, - "startup_tab": { - "ngs_map_icon_container": "Snapmap", - "ngs_chat_icon_container": "Chat", - "ngs_camera_icon_container": "Caméra", - "ngs_community_icon_container": "Communauté / Stories", - "ngs_spotlight_icon_container": "Spotlight", - "ngs_search_icon_container": "Recherche" - }, - "add_friend_source_spoof": { - "added_by_username": "Par nom d'utilisateur", - "added_by_mention": "Par mention", - "added_by_group_chat": "Par group d'amis", - "added_by_qr_code": "Par QR code", - "added_by_community": "Par communauté" + "ORIGINAL": "Originale" } } }, - "friend_menu_option": { - "preview": "Aperçu" + "preview": "Aperçu", + "stealth_mode": "Mode furtif", + "auto_download_blacklist": "Liste noire des téléchargements automatiques", + "anti_auto_save": "Empêcher l'enregistrement automatique" }, - "chat_action_menu": { "preview_button": "Aperçu", "download_button": "Télécharger", "delete_logged_message_button": "Supprimer le message enregistré" }, - "opera_context_menu": { "download": "Télécharger" }, - "modal_option": { "profile_info": "Informations du profil", "close": "Fermer" }, - "gallery_media_send_override": { "multiple_media_toast": "Vous ne pouvez envoyer qu'un seul média à la fois" }, - "conversation_preview": { - "streak_expiration": "Expire dans {day} jours {hour} heures {minute} minutes", - "total_messages": "Total des messages envoyés/reçus : {count}", - "title": "Aperçu de la conversation", + "streak_expiration": "expire dans {day} jour(s) {hour} heure(s) {minute} minute(s)", + "total_messages": "Total des messages envoyés/reçus : {count}", + "title": "Aperçu", "unknown_user": "Utilisateur inconnu" }, - "profile_info": { "title": "Informations du profil", - "username": "Nom d'utilisateur", "display_name": "Nom d'affichage", "added_date": "Date d'ajout", "birthday": "Anniversaire : {day} {month}" }, - "chat_export": { - "select_export_format": "Sélectionnez le format d'exportation", - "select_media_type": "Sélectionnez le type de média", - "select_amount_of_messages": "Sélectionnez la quantité de messages à exporter (laissez vide pour tout)", - "select_conversation": "Sélectionnez la conversation à exporter", "dialog_negative_button": "Annuler", - "dialog_neutral_button": "Exporter tout", "dialog_positive_button": "Exporter", "exported_to": "Exporté vers {path}", - "exporting_chats": "Exportation des conversations...", + "exporting_chats": "Exportation des chats...", "processing_chats": "Traitement de {amount} conversations...", - "export_fail": "Échec de l'exportation de la conversation {conversation}", - "writing_output": "Écriture du fichier de sortie...", - "finished": "Terminé ! Vous pouvez maintenant fermer cette fenêtre.", - "no_messages_found": "Aucun message trouvé", + "export_fail": "Échec de l'export de la conversation {conversation}", + "writing_output": "Écriture...", + "finished": "Terminé ! Vous pouvez maintenant fermer cette fenêtre.", + "no_messages_found": "Aucun message trouvé !", "exporting_message": "Exportation de {conversation}..." }, - "button": { - "ok": "OK", + "ok": "Ok", "positive": "Oui", "negative": "Non", "cancel": "Annuler", "open": "Ouvrir", "download": "Télécharger" }, - "profile_picture_downloader": { - "button": "Télécharger la photo de profil", - "title": "Télécharger la photo de profil", + "button": "Télécharger les photos de profil", + "title": "Téléchargeur de photos de profil", "avatar_option": "Avatar", "background_option": "Arrière-plan" }, - "download_processor": { + "attachment_type": { + "snap": "Snap", + "sticker": "Autocollant", + "external_media": "Média externe", + "note": "Note", + "original_story": "Storie originale" + }, + "select_attachments_title": "Choisir les pièces jointes à télécharger", "download_started_toast": "Téléchargement démarré", - "unsupported_content_type_toast": "Type de contenu non pris en charge", - "failed_no_longer_available_toast": "Le média n'est plus disponible", - "already_queued_toast": "Média déjà en file d'attente", - "already_downloaded_toast": "Média déjà téléchargé", - "saved_toast": "Sauvegardé dans {path}", - "download_toast": "Téléchargement de {path}...", + "unsupported_content_type_toast": "Type de contenu non supporté !", + "failed_no_longer_available_toast": "Média plus disponible", + "no_attachments_toast": "Aucune pièce jointe trouvée !", + "already_queued_toast": "Média déjà en file d'attente !", + "already_downloaded_toast": "Média déjà téléchargé !", + "saved_toast": "Enregistré dans {path}", + "download_toast": "Téléchargement {path}...", "processing_toast": "Traitement de {path}...", "failed_generic_toast": "Échec du téléchargement", - "failed_to_create_preview_toast": "Échec de la création de l'aperçu", + "failed_to_create_preview_toast": "Échec de création de l'aperçu", "failed_processing_toast": "Échec du traitement {error}", "failed_gallery_toast": "Échec de l'enregistrement dans la galerie {error}" }, - "streaks_reminder": { - "notification_title": "Flammes", - "notification_text": "Vous allez perdre vos flammes avec {friend} dans {hoursLeft} heures" + "notification_title": "Rappels des flammes", + "notification_text": "Vous perdrez votre série avec {friend} dans {hoursLeft} heures" } -} \ No newline at end of file +} diff --git a/common/src/main/assets/lang/gu_IN.json b/common/src/main/assets/lang/gu_IN.json new file mode 100644 index 000000000..5ddf2ea20 --- /dev/null +++ b/common/src/main/assets/lang/gu_IN.json @@ -0,0 +1,8 @@ +{ + "setup": { + "dialogs": { + "save_folder": "SnapEnhance ને તમારા મોબાઈલ ની સ્ટોરેજ ની આજ્ઞા જોઈએ છે મીડિયા ને ડાઉનલોડ અને મોબાઈલ માં સાચવવા માટે.\nક્રીપિય કરીને મોબાઈલ માં ક્યાં ડાઉનલોડ કરવું એ જગ્યા ચૂનો.", + "select_save_folder_button": "ફોલ્ડર ચૂનો" + } + } +} diff --git a/common/src/main/assets/lang/hi_IN.json b/common/src/main/assets/lang/hi_IN.json index 514c28421..8d9df134e 100644 --- a/common/src/main/assets/lang/hi_IN.json +++ b/common/src/main/assets/lang/hi_IN.json @@ -1,329 +1,47 @@ { - "category": { - "spying_privacy": "जासूसी और गोपनीयता", - "media_manager": "मीडिया प्रबंधक", - "ui_tweaks": "यूआई और ट्वीक्स", - "camera": "कैमरा", - "updates": "अपडेट", - "experimental_debugging": "प्रायोगिक" - }, - "action": { - "clean_cache": "कैश साफ़ करें", - "clear_message_logger": "संदेश लॉगर साफ़ करें", - "refresh_mappings": "मैपिंग रीफ़्रेश करें", - "open_map": "मैप पर स्थान चुनें", - "check_for_updates": "अपडेट जाँचें", - "export_chat_messages": "चैट संदेश निर्यात करें" - }, - "property": { - "message_logger": { - "name": "मैसेज लॉगर", - "description": "संदेशों को हटाने से रोकता है" - }, - "prevent_read_receipts": { - "name": "पठित प्राप्ति रोकें", - "description": "किसी को न बताएं कि आपने उनके स्नैप्स खोले हैं" - }, - "hide_bitmoji_presence": { - "name": "बिटमोजी मौजूदगी छिपाएं", - "description": "अपनी बिटमोजी मौजूदगी को चैट से छिपाएं" - }, - "better_notifications": { - "name": "बेहतर अधिसूचनाएँ", - "description": "अधिसूचनाओं में अधिक जानकारी दिखाएं" - }, - "notification_blacklist": { - "name": "अधिसूचना ब्लैकलिस्ट", - "description": "चयनित अधिसूचना प्रकार को छिपाएं" - }, - "disable_metrics": { - "name": "मैट्रिक्स अक्षम करें", - "description": "Snapchat को भेजे जाने वाले मैट्रिक्स को अक्षम करें" - }, - "block_ads": { - "name": "विज्ञापन रोकें", - "description": "विज्ञापनों को प्रदर्शित होने से रोकें" - }, - "unlimited_snap_view_time": { - "name": "असीमित स्नैप देखें समय", - "description": "स्नैप्स को देखने के लिए समय सीमा को हटा देता है" - }, - "prevent_sending_messages": { - "name": "संदेश भेजना रोकें", - "description": "कुछ प्रकार के संदेश भेजना रोके" - }, - "anonymous_story_view": { - "name": "गुमनाम कहानी देखें", - "description": "किसी को न बताएं कि आपने उनकी कहानी देखी है" - }, - "hide_typing_notification": { - "name": "टाइप करने की सूचना छिपाएं", - "description": "टाइप करने की सूचनाएँ भेजने से रोकें" - }, - "save_folder": { - "name": "सहेजें फ़ोल्डर", - "description": "सभी मीडिया को सहेजा जाने वाला निर्देशिका" - }, - "auto_download_options": { - "name": "ऑटो डाउनलोड विकल्प", - "description": "ऑटो डाउनलोड करने के लिए मीडिया का चयन करें" - }, - "download_options": { - "name": "डाउनलोड विकल्प", - "description": "फ़ाइल पथ प्रारूप निर्दिष्ट करें" - }, - "chat_download_context_menu": { - "name": "चैट डाउनलोड संदर्भ मेनू", - "description": "चैट डाउनलोड संदर्भ मेनू को सक्षम करें" - }, - "gallery_media_send_override": { - "name": "गैलरी मीडिया भेजें ओवरराइड", - "description": "गैलरी से भेजे गए मीडिया को ओवरराइड करें" - }, - "auto_save_messages": { - "name": "ऑटो संदेश सहेजें", - "description": "ऑटो सहेजने के लिए संदेश का प्रकार चुनें" - }, - "force_media_source_quality": { - "name": "मीडिया स्रोत गुणवत्ता को मजबूत करें", - "description": "मीडिया स्रोत क्वालिटी को ओवरराइड करें" - }, - "download_logging": { - "name": "डाउनलोड लॉग करे", - "description": "जब डाउनलोड हो रहा हो तब टोस्ट दिखाए" - }, - "enable_friend_feed_menu_bar": { - "name": "दोस्त फ़ीड मेनू बार सक्षम करें", - "description": "नए दोस्त फ़ीड मेनू बार को सक्षम करें" - }, - "friend_feed_menu_buttons": { - "name": "दोस्त फ़ीड मेनू बटन", - "description": "दोस्त फ़ीड मेनू बार में दिखाए जाने वाले बटन का चयन करें" - }, - "friend_feed_menu_buttons_position": { - "name": "दोस्त फ़ीड बटन स्थान सूचकांक", - "description": "दोस्त फ़ीड मेनू बटन का स्थान सूचकांक" - }, - "hide_ui_elements": { - "name": "यूआई तत्व छिपाएं", - "description": "छिपाने के लिए यूआई तत्व का चयन करें" - }, - "hide_story_section": { - "name": "स्टोरी खंड छिपाएं", - "description": "स्टोरी खंड में दिखाए जाने वाले कुछ यूआई तत्वों को छिपाएं" - }, - "story_viewer_override": { - "name": "स्टोरी देखने वाले को ओवरराइड करें", - "description": "Snapchat द्वारा छिपाए गए कुछ सुविधाओं को सक्षम करें" - }, - "streak_expiration_info": { - "name": "स्ट्रीक समाप्ति जानकारी दिखाएं", - "description": "स्ट्रीक के पास स्ट्रीक समाप्ति जानकारी दिखाएं" - }, - "disable_snap_splitting": { - "name": "स्नैप स्प्लिटिंग अक्षम करें", - "description": "स्नैप को एकाधिक भागों में विभाजित होने से रोकें" - }, - "disable_video_length_restriction": { - "name": "वीडियो लंबाई प्रतिबंध को अक्षम करें", - "description": "वीडियो लंबाई प्रतिबंध को अक्षम करता है" - }, - "snapchat_plus": { - "name": "स्नैपचैट प्लस", - "description": "स्नैपचैट प्लस सुविधाओं को सक्षम करता है" - }, - "new_map_ui": { - "name": "नई मानचित्र UI", - "description": "नई मानचित्र UI को सक्षम करता है" - }, - "location_spoof": { - "name": "स्नैपमैप स्थान स्पूफ़र", - "description": "स्नैपमैप पर अपने स्थान को स्पूफ़ करता है" - }, - "message_preview_length": { - "name": "संदेश पूर्वावलोकन लंबाई", - "description": "पूर्वावलोकित किए जाने वाले संदेशों की मात्रा निर्दिष्ट करें" - }, - "unlimited_conversation_pinning": { - "name": "असीमित बातचीत पिन करना", - "description": "असीमित बातचीत पिन करने की क्षमता को सक्षम करता है" - }, - "disable_spotlight": { - "name": "स्पॉटलाइट को अक्षम करें", - "description": "स्पॉटलाइट पेज को अक्षम करता है" - }, - "enable_app_appearance": { - "name": "ऐप उपस्थिति सेटिंग को सक्षम करें", - "description": "छिपे हुए ऐप उपस्थिति सेटिंग को सक्षम करता है" - }, - "startup_page_override": { - "name": "स्टार्टअप पेज को बदले", - "description": "स्टार्टअप पेज को बदले" - }, - "disable_google_play_dialogs": { - "name": "गूगल प्ले सर्विसेज़ की चेतावनी को रोके", - "description": "गूगल प्ले सर्विसेज़ की उपलब्धता संवादों को दिखने से रोकें" - }, - "auto_updater": { - "name": "ऑटो अपडेटर", - "description": "अपडेट के लिए जांच करने का अंतराल" - }, - "disable_camera": { - "name": "कैमरा को अक्षम करें", - "description": "स्नैपचैट को कैमरा का उपयोग करने से रोकता है" - }, - "immersive_camera_preview": { - "name": "विस्मरणीय कैमरा पूर्वावलोकन", - "description": "स्नैपचैट को कैमरा पूर्वावलोकन कट करने से रोकता है" - }, - "preview_resolution": { - "name": "पूर्वावलोकन रेज़ोल्यूशन", - "description": "कैमरा पूर्वावलोकन रेज़ोल्यूशन को ओवरराइड करता है" - }, - "picture_resolution": { - "name": "तस्वीर का रेज़ोल्यूशन", - "description": "तस्वीर का रेज़ोल्यूशन ओवरराइड करता है" - }, - "force_highest_frame_rate": { - "name": "सर्वोच्च फ्रेम दर को मजबूत करें", - "description": "सर्वाधिक संभव फ्रेम दर को मजबूत करता है" - }, - "force_camera_source_encoding": { - "name": "कैमरा स्रोत कोडिंग को मजबूत करें", - "description": "कैमरा स्रोत कोडिंग को मजबूत करता है" - }, - "app_passcode": { - "name": "ऐप पासकोड सेट करें", - "description": "ऐप को लॉक करने के लिए पासकोड सेट करता है" - }, - "app_lock_on_resume": { - "name": "ऐप लॉक ऑन रिज़्यूम", - "description": "ऐप को दोबारा खोलने पर लॉक करता है" - }, - "infinite_story_boost": { - "name": "अनंत कहानी बढ़ावा", - "description": "अपनी कहानी को अनंत बढ़ाएं" - }, - "meo_passcode_bypass": { - "name": "मेरी आंखों के लिए पासकोड बाईपास", - "description": "मेरी आंखों के लिए पासकोड बाईपास करें\nयह केवल तब काम करेगा जब पासकोड सही तरीके से दर्ज किया गया हो" - }, - "amoled_dark_mode": { - "name": "AMOLED डार्क मोड", - "description": "AMOLED डार्क मोड को सक्षम करता है\nसुनिश्चित करें कि स्नैपचैट का डार्क मोड सक्षम है" - }, - "unlimited_multi_snap": { - "name": "अनलिमिटेड मल्टी स्नैप भेजे", - "description": "आपको अनलिमिटेड मल्टी स्नैप भेजने दे" - }, - "device_spoof": { - "name": "डिवाइस की वैल्यू बदले", - "description": "डिवाइस की वैल्यू छुपाये" - }, - "device_fingerprint": { - "name": "डिवाइस की फिंगगर्प्रिन्ट", - "description": "डिवाइस की फिंगगर्प्रिन्ट बदले" - }, - "android_id": { - "name": "एंड्रॉयड आइडी", - "description": "एंड्रॉयड की आइडी बदले" + "setup": { + "dialogs": { + "select_language": "भाषा चुनें", + "select_save_folder_button": "फोल्डर चुनें" + }, + "mappings": { + "generate_button": "बनाएं", + "generate_failure_no_snapchat": "स्नैपएन्हांस के द्वारा स्नैपचैट को डिटेक्ट नहीं किया गया, कृपया स्नैपचैट दुबारा इंस्टॉल करें।", + "generate_failure": "मैपिंग बनाते समय एक त्रुटि पाई गई है, कृपया दुबारा कोसिस करें।", + "generate_success": "मैपिंग को सफलतापूर्वक बना लिया गया है।" + }, + "permissions": { + "dialog": "आगे बढ़ने के लिए आपको निम्नलिखित मांगो को पूरा करना पड़ेगा।:", + "notification_access": "नोटिफिकेशन अनुमति", + "battery_optimization": "बैटरी ऑप्टिमाइजेशन", + "display_over_other_apps": "दूसरे ऐप्लिकेशन के ऊपर दिखाए जाने का ऐक्सेस", + "request_button": "अनुरोध" } }, - "option": { - "property": { - "better_notifications": { - "chat": "चैट संदेश दिखाएं", - "snap": "मीडिया दिखाएं", - "reply_button": "जवाब बटन जोड़ें", - "download_button": "डाउनलोड बटन ऐड करे" - }, - "friend_feed_menu_buttons": { - "auto_download_blacklist": "⬇️ ऑटो डाउनलोड ब्लैकलिस्ट", - "anti_auto_save": "💬 आंतरिक ऑटो सेव संदेश", - "stealth_mode": "👻 स्टेल्थ मोड", - "conversation_info": "👤 वार्तालाप जानकारी" + "manager": { + "routes": { + "features": "फीचर्स", + "home": "होम", + "home_settings": "सेटिंग्स", + "social": "सामाजिक", + "scripts": "स्क्रिप्ट" + }, + "sections": { + "home": { + "logs": { + "clear_logs_button": "लॉग मिटाएं", + "export_logs_button": "लॉग एक्सपोर्ट करें" + } }, - "download_options": { - "allow_duplicate": "डुप्लिकेट डाउनलोड स्वीकार करें", - "create_user_folder": "प्रत्येक उपयोगकर्ता के लिए फ़ोल्डर बनाएं", - "append_hash": "फ़ाइल नाम में एक अद्वितीय हैश जोड़ें", - "append_username": "फ़ाइल नाम में उपयोगकर्ता नाम जोड़ें", - "append_date_time": "फ़ाइल नाम में तिथि और समय जोड़ें", - "append_type": "फ़ाइल नाम में मीडिया प्रकार जोड़ें", - "merge_overlay": "स्नैप छवि ओवरले मर्ज करें" + "features": { + "disabled": "निष्क्रिय" }, - "auto_download_options": { - "friend_snaps": "मित्र स्नैप्स", - "friend_stories": "मित्र की कहानियाँ", - "public_stories": "सार्वजनिक कहानियाँ", - "spotlight": "स्पॉटलाइट" - }, - "download_logging": { - "started": "शुरू हो गया", - "success": "सफलता", - "progress": "प्रगति", - "failure": "असफलता" - }, - "auto_save_messages": { - "NOTE": "ऑडियो नोट", - "CHAT": "चैट", - "EXTERNAL_MEDIA": "बाहरी मीडिया", - "SNAP": "स्नैप", - "STICKER": "स्टिकर" - }, - "notifications": { - "chat_screenshot": "स्क्रीनशॉट", - "chat_screen_record": "स्क्रीन रिकॉर्ड", - "camera_roll_save": "कैमरा में सेव करे", - "chat": "चैट", - "chat_reply": "चैट रिप्लाइ", - "snap": "स्नैप", - "typing": "टायपिंग", - "stories": "स्टोरीस", - "initiate_audio": "इनकमिंग ऑडियो कॉल", - "abandon_audio": "छूटी हुई ऑडियो कॉल", - "initiate_video": "इनकमिंग वीडियो कॉल", - "abandon_video": "छूटी हुई विडिओ कॉल" - }, - "gallery_media_send_override": { - "ORIGINAL": "मूल", - "NOTE": "ऑडियो नोट", - "SNAP": "स्नैप", - "LIVE_SNAP": "ऑडियो के साथ स्नैप" - }, - "hide_ui_elements": { - "remove_call_buttons": "कॉल बटन हटाएं", - "remove_cognac_button": "कोग्नाक बटन हटाएं", - "remove_live_location_share_button": "लाइव स्थान साझा करने वाला बटन हटाएं", - "remove_stickers_button": "स्टिकर बटन हटाएं", - "remove_voice_record_button": "आवाज रिकॉर्ड बटन हटाएं", - "remove_camera_borders": "कैमरा सीमाओं को हटाएं" - }, - "auto_updater": { - "DISABLED": "अक्षम", - "EVERY_LAUNCH": "हर लॉन्च पर", - "DAILY": "रोज़ाना", - "WEEKLY": "साप्ताहिक" - }, - "story_viewer_override": { - "OFF": "बंद", - "DISCOVER_PLAYBACK_SEEKBAR": "डिस्कवर प्लेबैक सीकबार सक्षम करें", - "VERTICAL_STORY_VIEWER": "वर्टिकल स्टोरी व्यूअर सक्षम करें" - }, - "hide_story_section": { - "hide_friend_suggestions": "मित्र सेक्शन छिपाएं", - "hide_friends": "मित्र सेक्शन छिपाएं", - "hide_following": "फ़ॉलोइंग सेक्शन छिपाएं", - "hide_for_you": "आपके लिए सेक्शन छिपाएं" - }, - "startup_page_override": { - "OFF": "बंद", - "ngs_map_icon_container": "मैप", - "ngs_chat_icon_container": "चैट", - "ngs_camera_icon_container": "कैमरा", - "ngs_community_icon_container": "समुदाय / स्टोरीस", - "ngs_spotlight_icon_container": "स्पॉटलाइट", - "ngs_search_icon_container": "ढुंढे" + "social": { + "e2ee_title": "एंड-टू-एंड एन्क्रिप्शन", + "rules_title": "नियम", + "participants_text": "{count} प्रतिभागी", + "not_found": "नहीं मिला", + "streaks_title": "स्ट्रीक" } } }, @@ -333,10 +51,6 @@ "auto_download_blacklist": "ऑटो डाउनलोड ब्लैकलिस्ट", "anti_auto_save": "ऑटो सेव के खिलाफ" }, - "message_context_menu_option": { - "download": "डाउनलोड करें", - "preview": "पूर्वावलोकन" - }, "chat_action_menu": { "preview_button": "पूर्वावलोकन", "download_button": "डाउनलोड करें", @@ -360,26 +74,12 @@ }, "profile_info": { "title": "प्रोफ़ाइल जानकारी", - "username": "उपयोगकर्ता नाम", "display_name": "प्रदर्शित नाम", "added_date": "जोड़ने की तारीख", "birthday": "जन्मदिन: {month} {day}" }, - "auto_updater": { - "no_update_available": "कोई अद्यतन उपलब्ध नहीं है!", - "dialog_title": "नया अद्यतन उपलब्ध है!", - "dialog_message": "SnapEnhance के लिए एक नया अद्यतन उपलब्ध है! ({version})\n\n{body}", - "dialog_positive_button": "डाउनलोड और स्थापित करें", - "dialog_negative_button": "रद्द करें", - "downloading_toast": "अद्यतन डाउनलोड हो रहा है...", - "download_manager_notification_title": "SnapEnhance APK डाउनलोड हो रहा है..." - }, "chat_export": { - "select_export_format": "निर्यात प्रारूप का चयन करें", - "select_media_type": "निर्यात करने के लिए मीडिया प्रकार का चयन करें", - "select_conversation": "निर्यात करने के लिए एक बातचीत का चयन करें", "dialog_negative_button": "रद्द करें", - "dialog_neutral_button": "सभी निर्यात करें", "dialog_positive_button": "निर्यात करें", "exported_to": "{path} में निर्यात किया गया", "exporting_chats": "बातचीत निर्यात कर रहा है...", @@ -397,33 +97,8 @@ "cancel": "रद्द करें", "open": "खोलें" }, - "download_manager_activity": { - "remove_all_title": "सभी डाउनलोड हटाएं", - "remove_all_text": "क्या आप इसे करना चाहते हैं?", - "remove_all": "सभी हटाएं", - "no_downloads": "कोई डाउनलोड नहीं", - "cancel": "रद्द करें", - "file_not_found_toast": "फ़ाइल मौजूद नहीं है!", - "category": { - "all_category": "सभी", - "pending_category": "अपूर्ण", - "snap_category": "स्नैप", - "story_category": "कहानी", - "spotlight_category": "स्पॉटलाइट" - }, - "debug_settings": "डीबग सेटिंग्स", - "debug_settings_page": { - "clear_file_title": "{file_name} फ़ाइल को साफ़ करें", - "clear_file_confirmation": "क्या आप वाकई {file_name} फ़ाइल को साफ़ करना चाहते हैं?", - "clear_cache_title": "कैश को साफ़ करें", - "reset_all_title": "सभी सेटिंग्स रीसेट करें", - "reset_all_confirmation": "क्या आप वाकई सभी सेटिंग्स को रीसेट करना चाहते हैं?", - "success_toast": "सफलता!", - "device_spoofer": "डिवाइस छुपाये" - } - }, "download_processor": { - "download_started_toast": "डाउनलोड शुरू...", + "download_started_toast": "डाउनलोड शुरू", "unsupported_content_type_toast": "असमर्थित फ़ाइल प्रकार!", "failed_no_longer_available_toast": "यह मीडिया अब उपलब्ध नहीं है", "already_queued_toast": "मीडिया पहले से कतार में है!", @@ -432,16 +107,6 @@ "download_toast": "डोनलोडिंग टू {path}...", "processing_toast": "प्रोसेसिंग {path}...", "failed_generic_toast": "डाउनलोड करने में असफल", - "failed_to_create_preview_toast": "प्रीव्यू करने में असफल", - "failed_processing_toast": "संसाधित करने में असफल {error}", - "failed_gallery_toast": "गॅलरी में सेव करने में असफल {error}" - }, - "config_activity": { - "title": "SnapEnhance सेटिंग्स", - "selected_text": "{count} चयनित", - "invalid_number_toast": "अमान्य संख्या!" - }, - "spoof_activity": { - "title": "स्पूफ सेटिंग्स" + "failed_to_create_preview_toast": "प्रीव्यू करने में असफल" } -} \ No newline at end of file +} diff --git a/common/src/main/assets/lang/hu_HU.json b/common/src/main/assets/lang/hu_HU.json index 029d4afec..0da71f938 100644 --- a/common/src/main/assets/lang/hu_HU.json +++ b/common/src/main/assets/lang/hu_HU.json @@ -1,341 +1,520 @@ { - "category": { - "spying_privacy": "Kémkedés & Adatvédelem", - "media_manager": "Médiakezelő", - "ui_tweaks": "Felhasználói felület és Módosítások", - "camera": "Kamera", - "updates": "Frissítések", - "experimental_debugging": "Kísérleti" + "setup": { + "dialogs": { + "select_language": "Nyelv Kiválasztása", + "save_folder": "A SnapEnhance-nek tárhelyhozzáférésre van szüksége média mentéséhez és letöltéséhez. \nKérem válassza ki a média mentési helyét.", + "select_save_folder_button": "Mappa Kiválasztása" + }, + "mappings": { + "dialog": "A Snapchat-verziók széles skálájának dinamikus támogatása érdekében a SnapEnhance megfelelő működéséhez leképezésekre van szükség, ez nem tarthat 5 másodpercnél tovább.", + "generate_button": "Generálás", + "generate_failure_no_snapchat": "A SnapEnhance nem találja a Snapchatet, próbálja meg újra telepíteni a Snapchatet.", + "generate_failure": "Hiba történt a beállítások mentése során, kérjük próbálja meg újra.", + "generate_success": "A leképezések sikeresen generálódtak." + }, + "permissions": { + "dialog": "A folytatáshoz a következőkre van szükség:", + "notification_access": "Értesítési hozzáférés", + "battery_optimization": "Akkumulátor Optimalizálás", + "display_over_other_apps": "Megjelenítés a többi alkalmazás fölött", + "request_button": "Kérelem" + } + }, + "manager": { + "routes": { + "features": "Funkciók", + "home": "Kezdőlap", + "home_settings": "Beállítások", + "home_logs": "Naplók", + "social": "Közösség", + "scripts": "Szkriptek" + }, + "sections": { + "home": { + "logs": { + "clear_logs_button": "Naplók ürítése", + "export_logs_button": "Naplók exportálása" + } + }, + "features": { + "disabled": "Letiltva" + }, + "social": { + "e2ee_title": "Végpontok közötti titkosítás", + "rules_title": "Szabályok", + "participants_text": "{count} résztvevő", + "not_found": "Nem található", + "streaks_title": "Sorozatok", + "streaks_length_text": "Hossz: {length}", + "streaks_expiration_short": "{hours} óra", + "streaks_expiration_text": "{eta} múlva jár le", + "reminder_button": "Emlékeztető beállítása" + } + }, + "dialogs": { + "add_friend": { + "title": "Barát vagy csoport hozzáadása", + "search_hint": "Keresés", + "fetch_error": "Sikertelen adatlekérdezés", + "category_groups": "Csoportok", + "category_friends": "Barátok" + } + } }, - "action": { - "clean_cache": "Gyorsítótár Törlése", + "rules": { + "modes": { + "blacklist": "Feketelista mód", + "whitelist": "Whitelist mód" + }, + "properties": { + "auto_download": { + "name": "Automatikus letöltés", + "description": "Automatikusan letölti a Snap-eket a megtekintésükkor", + "options": { + "blacklist": "Automatikus letöltésből való kizárás", + "whitelist": "Automatikus letöltés" + } + }, + "stealth": { + "name": "Rejtőzködő üzemmód", + "description": "Megakadályozza, hogy bárki megtudja, hogy megnyitotta a Snap-eket/Chat-eket és beszélgetéseket", + "options": { + "blacklist": "Kizárás a rejtőzködő üzemmódból", + "whitelist": "Rejtőzködő üzemmód" + } + }, + "auto_save": { + "name": "Automatikus mentés", + "description": "Elmenti a Chat Üzeneteket megtekintéskor", + "options": { + "blacklist": "Automatikus mentésből kizárás", + "whitelist": "Automatikus mentés" + } + }, + "hide_friend_feed": { + "name": "Elrejtés a barát hírfolyamáról" + }, + "e2e_encryption": { + "name": "Végpontok közötti titkosítás használata" + }, + "pin_conversation": { + "name": "Beszélgetés kitűzése" + }, + "unsaveable_messages": { + "name": "Nem menthető üzenetek", + "options": { + "whitelist": "Nem menthető üzenetek" + } + } + } + }, + "actions": { + "clean_snapchat_cache": "A Snapchat gyorsítótárának ürítése", "clear_message_logger": "Üzenetnapló Törlése", "refresh_mappings": "Hozzárendelések frissítése", "open_map": "Válassz egy helyet a térképen", - "check_for_updates": "Frissítések keresése", - "export_chat_messages": "Chat üzenetek exportálása" + "check_for_updates": "Frissítés keresése", + "export_chat_messages": "Chat üzenetek exportálása", + "export_memories": "Emlékek exportálása" }, - "property": { - "message_logger": { - "name": "Üzenetnapló", - "description": "Megakadályozza az üzenetek törlését" - }, - "prevent_read_receipts": { - "name": "Olvasási Visszaigazolások Megakadályozása", - "description": "Megakadályozhatja, hogy bárki megtudja, hogy megnyitottad a Snapjeiket" - }, - "hide_bitmoji_presence": { - "name": "Bitmojik elrejtése", - "description": "Elrejti a Bitmojidat a beszélgetésből" - }, - "better_notifications": { - "name": "Jobb Értesítések", - "description": "Több információt mutat az értesítésekben" - }, - "notification_blacklist": { - "name": "Értesítési Tiltólista", - "description": "Elrejti a kiválasztott típusú értesítéseket" - }, - "disable_metrics": { - "name": "Mérések letiltása", - "description": "Letiltja a Snapchatnek küldött adatokat" - }, - "block_ads": { - "name": "Hirdetések Blokkolása", - "description": "Letiltja a megjelenő hirdetéseket" - }, - "unlimited_snap_view_time": { - "name": "Korlátlan Snap Megtekintés", - "description": "Eltávolítja a Snap megtekintési időkorlátot" - }, - "prevent_sending_messages": { - "name": "Üzenetek Küldésének Megakadályozása", - "description": "Megakadályozza bizonyos típusú üzenenetek küldését" - }, - "anonymous_story_view": { - "name": "Anonim Story nézés", - "description": "Megakadályozza, hogy megtudják, hogy láttad a történetüket" - }, - "hide_typing_notification": { - "name": "Gépelési értesítés elrejtése", - "description": "Megakadályozza a gépelési értesítések küldését" - }, - "save_folder": { - "name": "Letöltések mappa", - "description": "A mappa, ahová az összes médiát lemented" - }, - "auto_download_options": { - "name": "Automatikus letöltési beállítások", - "description": "Válaszd ki az automatikusan letölthető médiákat" - }, - "download_options": { - "name": "Letöltési beállítások", - "description": "A fájl elérési útvonal formátumának megadása" - }, - "chat_download_context_menu": { - "name": "Chat letöltés kontextusmenü", - "description": "A chat letöltés kontextusmenüjének engedélyezése" - }, - "gallery_media_send_override": { - "name": "Galéria Média küldés felülbírálása", - "description": "A galériából küldött média felülírása" - }, - "auto_save_messages": { - "name": "Üzenetek automatikus mentése", - "description": "Válaszd ki az automatikusan mentendő üzenetek típusát" - }, - "force_media_source_quality": { - "name": "Médiaforrás minőségének kikényszerítése", - "description": "Felülírja a médiaforrás minőségét" - }, - "download_logging": { - "name": "Napló letöltése", - "description": "Felugró buborék mutatása média letöltésekor" - }, - "enable_friend_feed_menu_bar": { - "name": "Barát beállítások menüsor", - "description": "Engedélyezi az új barát beállítások menüsort" - }, - "friend_feed_menu_buttons": { - "name": "Barát beállítások menü gombok", - "description": "Válaszd ki, hogy mely gombok jelenjenek meg a barát beállítások menüsorban" - }, - "friend_feed_menu_buttons_position": { - "name": "Barát beállítások pozíciójának indexe", - "description": "A barát beállítások menügombok pozíciója" - }, - "hide_ui_elements": { - "name": "UI Elemek elrejtése", - "description": "Válaszd ki, mely UI-elemeket szeretnéd elrejteni" - }, - "hide_story_section": { - "name": "Sztori rész elrejtése", - "description": "Bizonyos UI-elemek elrejtése a történet szekcióban" - }, - "story_viewer_override": { - "name": "Történet megtekintő felülírása", - "description": "Bekapcsol bizonyos funkciókat, amelyeket a Snapchat elrejtett" - }, - "streak_expiration_info": { - "name": "Mutassa a streak lejárati adatait", - "description": "Megjeleníti a streak lejárati adatait a streak-ek mellett" - }, - "disable_snap_splitting": { - "name": "Snap osztás kikapcsolása", - "description": "Megakadályozza a Snapek több részre osztását" - }, - "disable_video_length_restriction": { - "name": "Videóhossz-korlátozás letiltása", - "description": "Letiltja a videó hosszára vonatkozó korlátozásokat" - }, - "snapchat_plus": { - "name": "Snapchat Plusz", - "description": "Engedélyezi a Snapchat Plusz funkciókat" - }, - "new_map_ui": { - "name": "Új térkép UI", - "description": "Engedélyezi a térkép új UI" - }, - "location_spoof": { - "name": "Snapmap helymeghatározás átállító", - "description": "Meghamisítja az tartózkodási helyed a Snapmap-en" - }, - "message_preview_length": { - "name": "Üzenet előnézet hossza", - "description": "Az előnézetben megjelenítendő üzenetek mennyiségének megadása" - }, - "unlimited_conversation_pinning": { - "name": "Korlátlan beszélgetés kitűzés", - "description": "Lehetővé teszi korlátlan számú beszélgetés kitűzését" - }, - "disable_spotlight": { - "name": "Spotlight letiltása", - "description": "Letiltja a Spotlight oldalt" - }, - "enable_app_appearance": { - "name": "Az alkalmazás megjelenési beállításainak engedélyezése", - "description": "Engedélyezi a rejtett alkalmazás megjelenési beállításait" - }, - "startup_page_override": { - "name": "Kezdőoldal felülírása", - "description": "Felülírja az indítóoldalt" - }, - "disable_google_play_dialogs": { - "name": "A Google Play Szolgáltatások Párbeszédablakainak Letiltása", - "description": "Elrejti a Google Play Szolgáltatások elérhetőségére vonatkozó párbeszédablakokat" - }, - "auto_updater": { - "name": "Automatikus frissítő", - "description": "A frissítések ellenőrzésének időintervalluma" - }, - "disable_camera": { - "name": "Kamera letiltása", - "description": "Megakadályozza, hogy a Snapchat használhassa a kamerát" - }, - "immersive_camera_preview": { - "name": "Merev kamera előnézet", - "description": "Megakadályozza, hogy a Snapchat levágja a kamera előnézetét" - }, - "preview_resolution": { - "name": "Előnézet Felbontás", - "description": "Felülírja a kamera előnézeti felbontását" - }, - "picture_resolution": { - "name": "Képfelbontás", - "description": "Felülírja a képfelbontást" - }, - "force_highest_frame_rate": { - "name": "Legmagasabb képkocka sebesség kikényszerítése", - "description": "A lehető legmagasabb képkocka sebességet kényszeríti ki" - }, - "force_camera_source_encoding": { - "name": "Kamera forráskódolás kikényszerítése", - "description": "Kényszeríti a kamera forráskódolását" - }, - "app_passcode": { - "name": "App jelszó beállítása", - "description": "Beállít egy jelszót az alkalmazás zárolásához" - }, - "app_lock_on_resume": { - "name": "Alkalmazás zárolása folytatáskor", - "description": "Zárolja az alkalmazást az újbóli megnyitásakor" - }, - "infinite_story_boost": { - "name": "Végtelen történet Boost", - "description": "Végtelenül feldobja a történetedet" - }, - "meo_passcode_bypass": { - "name": "My Eyes Only jelszó megkerülése", - "description": "A My Eyes Only jelszó megkerülése\nEz csak akkor működik, ha a jelszót korábban helyesen adta meg" - }, - "amoled_dark_mode": { - "name": "AMOLED Sötét mód", - "description": "Engedélyezi az AMOLED sötét üzemmódot\nGyőződj meg róla, hogy a Snapchat sötét módja engedélyezve van" - }, - "unlimited_multi_snap": { - "name": "Korlátlan Multi Snap", - "description": "Lehetővé teszi, hogy korlátlan számú többszörös pillanatfelvételt készítsen" - }, - "device_spoof": { - "name": "Eszközértékek hamisítása", - "description": "Meghamisítja az eszközök értékeit" - }, - "device_fingerprint": { - "name": "Eszköz Ujjlenyomata", - "description": "Eszköz ujjlenyomatának meghamisítása" + "features": { + "notices": { + "unstable": "⚠ Instabil", + "ban_risk": "⚠ Ez a funkció tiltást eredményezhet", + "internal_behavior": "⚠ Ez a funkció hibát okozhat a Snapchat működésében" + }, + "properties": { + "downloader": { + "name": "Letöltő", + "description": "Snapchat média letöltése", + "properties": { + "save_folder": { + "name": "Letöltések mappa", + "description": "Válaszd ki a könyvtárat ahová a letöltött médiát szeretnéd menteni" + }, + "auto_download_sources": { + "name": "Automatikus letöltési források", + "description": "Válaszd ki a forrásokat ahonnan automatikusan leszeretnél tölteni" + }, + "prevent_self_auto_download": { + "name": "Sajátmagam automatikus letöltésének megakadályozása", + "description": "Megakadályozza hogy a saját Snap-jeid automatikusan le legyenek töltve" + }, + "path_format": { + "name": "Elérési útvonal formátuma", + "description": "A fájl elérési útvonal formátumának megadása" + }, + "allow_duplicate": { + "name": "Ismétlődések engedélyezése", + "description": "Engedélyezi a médiák többszöri letöltését" + }, + "force_image_format": { + "name": "Képformátum kényszerítése", + "description": "Kényszeríti a képeket hogy egy megadott formátumba legyenek mentve" + }, + "force_voice_note_format": { + "name": "Hangüzenet formátumának kényszerítése", + "description": "Kényszeríti a hangüzeneteket hogy egy megadott formátumba legyenek mentve" + }, + "download_profile_pictures": { + "name": "Profilképek letöltése", + "description": "Lehetővé teszi hogy letölts profilképeket a profil oldaláról" + }, + "chat_download_context_menu": { + "name": "Chat letöltés kontextusmenü", + "description": "Lehetővé teszi a média letöltését hosszú nyomással" + }, + "ffmpeg_options": { + "name": "FFmpeg beállítások", + "description": "További FFmpeg beállítások meghatározása", + "properties": { + "preset": { + "name": "Sablon", + "description": "Beszélgetés gyorsaságának állítása" + }, + "video_bitrate": { + "name": "Videó bitrátája", + "description": "A videó bitrátájának állítása (kbps)" + }, + "audio_bitrate": { + "name": "Hang bitrátája", + "description": "A hang bitrátájának állítása (kbps)" + }, + "custom_video_codec": { + "name": "Egyéni videó codec", + "description": "Egyéni videó codec beállítása (pl.: libx264)" + }, + "custom_audio_codec": { + "name": "Egyéni audió codec", + "description": "Egyéni audió codec beállítása (pl.: AAC)" + } + } + }, + "logging": { + "name": "Naplózás" + }, + "opera_download_button": { + "description": "Egy letöltés gombot helyez a jobb felső sarokba Snap-ek megtekintése közben", + "name": "Letöltés Operával Gomb" + }, + "merge_overlays": { + "description": "Egyesíti egy Snap szövegét és médiáját egy fájlba" + } + } + }, + "user_interface": { + "name": "Felhasználói felület", + "properties": { + "enable_app_appearance": { + "name": "Az alkalmazás megjelenési beállításainak engedélyezése" + }, + "amoled_dark_mode": { + "name": "AMOLED Sötét mód", + "description": "Engedélyezi az AMOLED sötét üzemmódot\nGyőződj meg róla, hogy a Snapchat sötét módja engedélyezve van" + }, + "friend_feed_message_preview": { + "name": "Barát hírfolyam üzenet előnézet", + "description": "Megmutatja az előnézetét az utolsó üzeneteknek a barát hírfolyamban", + "properties": { + "amount": { + "name": "Mennyiség", + "description": "Az előnézetben megjelenítendő üzenetek mennyisége" + } + } + }, + "bootstrap_override": { + "properties": { + "app_appearance": { + "name": "App Megjelenése" + }, + "home_tab": { + "name": "Kezdőlap", + "description": "Felülírja a megjelenő ablakot a Snapchat indulásakor" + } + }, + "description": "Felülírja a kezelőfelület Bootstrap beállításait" + }, + "streak_expiration_info": { + "name": "Mutassa a streak lejárati adatait" + }, + "hide_friend_feed_entry": { + "name": "Barát hírfolyam elrejtése", + "description": "Elrejt egy kiválasztott barátot a barát hírfolyamról\nHasználd a közösségi lapot ahhoz hogy kezeld ezt a funkciót" + }, + "hide_streak_restore": { + "name": "Sorozat pontszám elrejtése", + "description": "Elrejti a visszaállítás gombot a barát hírfolyamról" + }, + "hide_story_sections": { + "name": "Történetek elrejtése", + "description": "A felhasználói felület egyes elemeinek elrejtése a történet szekcióban" + }, + "hide_ui_components": { + "name": "Felhasználói felület elemeinek elrejtése", + "description": "Válaszd ki hogy a felhasználói felület mely részeit szeretnéd elrejteni" + }, + "disable_spotlight": { + "name": "Spotlight letiltása", + "description": "Letiltja a Spotlight oldalt" + }, + "friend_feed_menu_buttons": { + "name": "Barát hírfolyam menü gombok", + "description": "Annak kiválasztása hogy mely gombok jelennek meg a barát hírfolyam menüsorban" + }, + "friend_feed_menu_position": { + "name": "Barát hírfolyam pozíciójának indexe", + "description": "A barát hírfolyam menügombok pozíciója" + }, + "enable_friend_feed_menu_bar": { + "name": "Barát hírfolyam menüsor", + "description": "Engedélyezi az új barát hírfolyam menüsort" + }, + "prevent_message_list_auto_scroll": { + "name": "Megakadályozza az üzenet lista automatikus görgetését" + }, + "snap_preview": { + "name": "Snap előnézet", + "description": "Megjelenít egy kis előnézetet a chatben a nem látott Snapek mellett" + } + }, + "description": "Módosítsd a Snapchat kinézetét" + }, + "messaging": { + "name": "Üzenetküldés", + "properties": { + "anonymous_story_viewing": { + "name": "Anonim Story nézés", + "description": "Megakadályozza, hogy bárki megtudja, hogy láttad a történetüket" + }, + "hide_bitmoji_presence": { + "name": "Bitmojik elrejtése", + "description": "Megakadályozza a Bitmoji-d megjelenítését, miközben a Chat meg van nyitva" + }, + "hide_typing_notifications": { + "name": "Gépelési értesítések elrejtése", + "description": "Megakadályozza, hogy bárki megtudja, hogy éppen üzenetet írsz" + }, + "unlimited_snap_view_time": { + "name": "Korlátlan Snap Megtekintési Idő", + "description": "Eltávolítja a Snap-ek megtekintési időkorlátját" + }, + "disable_replay_in_ff": { + "description": "Letiltja a lehetőséget hogy újrajátsz a barát hírfolyamból hosszú nyomással" + }, + "better_notifications": { + "name": "Jobb Értesítések", + "description": "Több információt mutat az értesítésekben" + }, + "notification_blacklist": { + "name": "Értesítési Tiltólista" + }, + "message_logger": { + "name": "Üzenetnapló", + "description": "Megakadályozza az üzenetek törlését" + }, + "auto_save_messages_in_conversations": { + "name": "Üzenetek automatikus mentése", + "description": "Automatikusan elment minden üzenetet a beszélgetésekben" + }, + "gallery_media_send_override": { + "name": "Galéria Média küldés felülbírálása", + "description": "Meghamisítja a média forrását amikor a gallériából küldöd" + } + } + }, + "global": { + "name": "Globális", + "properties": { + "spoofLocation": { + "name": "Tartózkodási hely", + "description": "Tartózkodási helyed hamisítása", + "properties": { + "coordinates": { + "name": "Koordináták", + "description": "Koordináták beállítása" + } + } + }, + "snapchat_plus": { + "name": "Snapchat Plusz", + "description": "Engedélyezi a Snapchat Plusz funkciókat\nNéhány szerver oldali funkció lehetséges hogy nem működik" + }, + "auto_updater": { + "name": "Automatikus frissítő", + "description": "Automatikusan keres új frissítéseket" + }, + "disable_metrics": { + "name": "Mérések letiltása", + "description": "Blokkolja az elemző adatküldést a Snapchat számára" + }, + "block_ads": { + "name": "Hirdetések Blokkolása", + "description": "Megakadályozza a reklámok megjelenítését" + }, + "bypass_video_length_restriction": { + "name": "Videóhossz-korlátozás megkerülése" + }, + "disable_google_play_dialogs": { + "name": "A Google Play Szolgáltatások Párbeszédablakainak Letiltása", + "description": "Megakadályozza a Google Play Szolgáltatások elérhetőségére vonatkozó párbeszédablakok megjelenítését" + }, + "disable_snap_splitting": { + "name": "Snap szétválasztás kikapcsolása", + "description": "Megakadályozza a Snap-ek több részre történő szétválasztását\nA képek amiket küldesz videók lesznek" + } + } + }, + "rules": { + "name": "Szabályok" + }, + "camera": { + "name": "Kamera", + "properties": { + "disable_camera": { + "name": "Kamera letiltása" + }, + "override_preview_resolution": { + "name": "Előnézeti képfelbontás felülírása", + "description": "Felülírja a kamera előnézeti felbontását" + }, + "override_picture_resolution": { + "name": "Képfelbontás felülírása", + "description": "Felülírja a képfelbontást" + } + } + }, + "streaks_reminder": { + "properties": { + "remaining_hours": { + "name": "Hátralévő idő" + } + } + }, + "experimental": { + "name": "Kísérleti", + "description": "Kísérleti funkciók", + "properties": { + "native_hooks": { + "properties": { + "disable_bitmoji": { + "name": "Bitmoji kikapcsolása", + "description": "Barátok Bitmoji-jának kikapcsolása" + } + } + }, + "spoof": { + "name": "Hamisítás", + "description": "Meghamisít számos információt rólad" + }, + "app_passcode": { + "name": "Alkalmazás jelszó", + "description": "Beállít egy jelszót az alkalmazás zárolásához" + }, + "app_lock_on_resume": { + "name": "Alkalmazás zárolása folytatáskor", + "description": "Zárolja az alkalmazást az újbóli megnyitásakor" + }, + "infinite_story_boost": { + "name": "Végtelen történet Boost", + "description": "Történet Boost limit megkerülése" + }, + "e2ee": { + "name": "Végpontok közötti titkosítás", + "description": "Titkosítja az üzeneteidet AES-el egy megosztott titkos kulcsot használva\nBizonyosodj meg róla hogy lementsd a kulcsodat egy biztonságos helyre!", + "properties": { + "encrypted_message_indicator": { + "name": "Titkosított Üzenet Jelző", + "description": "Hozzáad egy 🔒 emoji-t a titkosított üzenetek mellé" + }, + "force_message_encryption": { + "name": "Üzenet titkosítás kényszerítése" + } + } + }, + "hidden_snapchat_plus_features": { + "name": "Engedélyezi a rejtett Snapchat Plusz funkciókat", + "description": "Engedélyezi a még nem megjelent/béta Snapchat Plusz funkciókat\nRégebbi Snapchat verziókon lehetséges hogy nem működik" + } + } + }, + "scripting": { + "properties": { + "developer_mode": { + "name": "Fejlesztői mód" + } + } + } }, - "android_id": { - "name": "Android Azonosítója", - "description": "Meghamísítja az eszköz Android Azonosítóját" - } - }, - "option": { - "property": { + "options": { + "app_appearance": { + "always_light": "Mindig világos", + "always_dark": "Mindig sötét" + }, "better_notifications": { - "chat": "Chat-üzenetek megjelenítése", - "snap": "Média megjelenítése", "reply_button": "Válasz gomb hozzáadása", "download_button": "Letöltés gomb hozzáadása" }, "friend_feed_menu_buttons": { - "auto_download_blacklist": "⬇️ Automatikus letöltési kivételek", - "anti_auto_save": "💬 Ne mentse le automatikusan az üzeneteket", - "stealth_mode": "👻 Lopakodó üzemmód", - "conversation_info": "👤 Beszélgetés információk" - }, - "download_options": { - "allow_duplicate": "Duplikált letöltések engedélyezése", - "create_user_folder": "Mappa létrehozása minden felhasználó számára", - "append_hash": "Egyedi hash hozzáadása a fájlnévhez", - "append_username": "Felhasználónév hozzáadása a fájl nevéhez", - "append_date_time": "Dátum és idő hozzáadása a fájl nevéhez", - "append_type": "Média típusa hozzáadása a fájl nevéhez", - "merge_overlay": "Snap képfelületek összevonása" + "auto_download": "⬇️ Automatikus letöltés", + "auto_save": "💬 Üzenetek automatikus mentése", + "stealth": "👻 Lopakodó üzemmód", + "conversation_info": "👤 Beszélgetés információk", + "e2e_encryption": "🔒 Végpontok közötti titkosítás használata" }, - "auto_download_options": { + "auto_download_sources": { "friend_snaps": "Barát Snap-ek", "friend_stories": "Barát történetek", - "public_stories": "Nyilvános történeket", + "public_stories": "Nyilvános történetek", "spotlight": "Spotlight" }, - "download_logging": { + "logging": { "started": "Elkezdődött", - "success": "Sikeres", - "progress": "Folyamat", + "success": "Siker", + "progress": "Előrehaladás", "failure": "Sikertelen" }, - "auto_save_messages": { - "NOTE": "Hangüzenet", - "CHAT": "Üzenet", - "EXTERNAL_MEDIA": "Külső média", - "SNAP": "Snap", - "STICKER": "Matrica" - }, "notifications": { "chat_screenshot": "Képernyőkép", "chat_screen_record": "Képernyőfelvétel", - "camera_roll_save": "Camera Roll mentése", - "chat": "Üzenet", + "snap_replay": "Snap újrajátszás", + "chat": "Chat", "chat_reply": "Chat Válasz", "snap": "Snap", - "typing": "Gépelés", + "typing": "Gépel", "stories": "Történetek", - "initiate_audio": "Bejövő Hanghívás", - "abandon_audio": "Nem Fogadott Hanghívás", - "initiate_video": "Bejövő Videohívás", - "abandon_video": "Nem Fogadott Videohívás" + "initiate_audio": "Bejövő hívás", + "abandon_audio": "Nem fogadott hívás", + "initiate_video": "Bejövő videóhívás", + "abandon_video": "Nem fogadott videóhívás" }, "gallery_media_send_override": { "ORIGINAL": "Eredeti", - "NOTE": "Hangüzenet", - "SNAP": "Snap", - "LIVE_SNAP": "Snap hanggal" - }, - "hide_ui_elements": { - "remove_call_buttons": "Hívás gombok eltávolítása", - "remove_cognac_button": "Cognac gomb eltávolítása", - "remove_live_location_share_button": "Élő helymegosztó gomb eltávolítása", - "remove_stickers_button": "Matricák eltávolítása gomb", - "remove_voice_record_button": "Hangfelvétel gomb eltávolítása", - "remove_camera_borders": "Kamerakeret eltávolítása" - }, - "auto_updater": { - "DISABLED": "Kikapcsolva", - "EVERY_LAUNCH": "Minden indításnál", - "DAILY": "Naponta", - "WEEKLY": "Hetente" + "SNAP": "Snap" }, - "story_viewer_override": { - "OFF": "Ki", - "DISCOVER_PLAYBACK_SEEKBAR": "A lejátszás felfedezésének engedélyezése", - "VERTICAL_STORY_VIEWER": "Függőleges történetnézegető engedélyezése" + "hide_ui_components": { + "hide_chat_call_buttons": "Hívás gombok eltávolítása", + "hide_voice_record_button": "Hangfelvétel gomb eltávolítása" }, - "hide_story_section": { + "hide_story_sections": { "hide_friend_suggestions": "Barátjavaslatok elrejtése", "hide_friends": "Barátok szekció elrejtése", - "hide_following": "Követések szakasz elrejtése", + "hide_suggested": "Javasolt szekció elrejtése", "hide_for_you": "Neked szakasz elrejtése" }, - "startup_page_override": { - "OFF": "Ki", - "ngs_map_icon_container": "Térkép", - "ngs_chat_icon_container": "Üzenetek", - "ngs_camera_icon_container": "Kamera", - "ngs_community_icon_container": "Közösség / Történetek", - "ngs_spotlight_icon_container": "Spotlight", - "ngs_search_icon_container": "Keresés" + "home_tab": { + "map": "Térkép", + "chat": "Chat", + "camera": "Kamera", + "discover": "Felfedezés", + "spotlight": "Spotlight" } } }, "friend_menu_option": { "preview": "Előnézet", - "stealth_mode": "Lopakodás mód", - "auto_download_blacklist": "Automatikus letöltési kivételek", - "anti_auto_save": "Ne mentse automatikusan" - }, - "message_context_menu_option": { - "download": "Letöltés", - "preview": "Előnézet" + "stealth_mode": "Lopakodó Üzemmód", + "auto_download_blacklist": "Automatikus Letöltési Kivételek", + "anti_auto_save": "Automatikus Mentés Megakadályozása" }, "chat_action_menu": { "preview_button": "Előnézet", @@ -360,26 +539,20 @@ }, "profile_info": { "title": "Profil Infó", - "username": "Felhasználónév", + "mutable_username": "Némítható felhasználónév", "display_name": "Megjelenített név", "added_date": "Hozzáadás dátuma", - "birthday": "Születésnap : {month} {day}" - }, - "auto_updater": { - "no_update_available": "Nincs elérhető frissítés!", - "dialog_title": "Új frissítés elérhető!", - "dialog_message": "Új frissítés érhető el a SnapEnhance számára! ({version})\n\n{body}", - "dialog_positive_button": "Letöltés és telepítés", - "dialog_negative_button": "Mégsem", - "downloading_toast": "Frissítés letöltése...", - "download_manager_notification_title": "SnapEnhance APK letöltése..." + "birthday": "Születésnap : {month} {day}", + "friendship": "Barátság", + "add_source": "Forrás hozzáadása", + "snapchat_plus": "Snapchat Plusz", + "snapchat_plus_state": { + "subscribed": "Feliratkozva", + "not_subscribed": "Nincs feliratkozás" + } }, "chat_export": { - "select_export_format": "Válassz export formátumot", - "select_media_type": "Válaszd ki az exportálandó médiatípusokat", - "select_conversation": "Válaszd ki az exportálandó beszélgetést", "dialog_negative_button": "Mégsem", - "dialog_neutral_button": "Minden exportálása", "dialog_positive_button": "Exportálás", "exported_to": "Exportálva ide {path}", "exporting_chats": "Üzenetek exportálása...", @@ -395,37 +568,28 @@ "positive": "Igen", "negative": "Nem", "cancel": "Mégsem", - "open": "Megnyitás" + "open": "Megnyitás", + "download": "Letöltés" }, - "download_manager_activity": { - "remove_all_title": "Összel letöltés eltávolítása", - "remove_all_text": "Biztos vagy benne?", - "remove_all": "Eltávolítás", - "no_downloads": "Nincsenek letöltések", - "cancel": "Mégsem", - "file_not_found_toast": "A fájl nem létezik!", - "category": { - "all_category": "Összes", - "pending_category": "Függőben", - "snap_category": "Snapek", - "story_category": "Sztorik", - "spotlight_category": "Spotlight" - }, - "debug_settings": "Hibakeresési beállítások", - "debug_settings_page": { - "clear_file_title": "{file_name} fájl törlése", - "clear_file_confirmation": "Biztos, hogy törölni akarod a {file_name} fájlt?", - "clear_cache_title": "Gyorsítótár törlése", - "reset_all_title": "Beállítások visszaállítása", - "reset_all_confirmation": "Biztos alapra szeretnéd helyezni?", - "success_toast": "Siker!", - "device_spoofer": "Eszköz hamisító" - } + "profile_picture_downloader": { + "button": "Profilkép letöltése", + "title": "Profilkép letöltő", + "avatar_option": "Avatár", + "background_option": "Háttér" }, "download_processor": { + "attachment_type": { + "snap": "Snap", + "sticker": "Matrica", + "external_media": "Külső média", + "note": "Megjegyzés", + "original_story": "Eredeti történet" + }, + "select_attachments_title": "Válaszd ki a letöltendő csatolmányokat", "download_started_toast": "A letöltés megkezdődött", "unsupported_content_type_toast": "A tartalom típusa nem támogatott!", "failed_no_longer_available_toast": "A média már nem érhető el", + "no_attachments_toast": "Nem találhatóak csatolmányok!", "already_queued_toast": "A média már várólistán van!", "already_downloaded_toast": "Ez már le lett töltve!", "saved_toast": "Lementve {path}", @@ -436,12 +600,16 @@ "failed_processing_toast": "Nem sikerült feldolgozni {error}", "failed_gallery_toast": "Nem sikerült lementeni a galériába {error}" }, - "config_activity": { - "title": "SnapEnhance Beállítások", - "selected_text": "{count} kiváltasztva", - "invalid_number_toast": "Érvénytelen szám!" + "streaks_reminder": { + "notification_title": "Sorozatok", + "notification_text": "El fogod veszíteni a sorozatodat {friend}-el/al {hoursLeft} múlva" }, - "spoof_activity": { - "title": "Hamisítási Beállítások" + "material3_strings": { + "date_input_invalid_not_allowed": "Helytelen dátum", + "date_range_input_invalid_range_input": "Helytelen dátum intervallum", + "date_range_picker_day_in_range": "Kiválasztott", + "date_input_invalid_for_pattern": "Helytelen dátum", + "date_picker_today_description": "Ma", + "date_input_invalid_year_range": "Helytelen év" } -} \ No newline at end of file +} diff --git a/common/src/main/assets/lang/id.json b/common/src/main/assets/lang/id.json new file mode 100644 index 000000000..ce63c5853 --- /dev/null +++ b/common/src/main/assets/lang/id.json @@ -0,0 +1,40 @@ +{ + "setup": { + "dialogs": { + "select_language": "Pilih bahasa", + "save_folder": "SnapEnhance memerlukan izin Penyimpanan untuk mengunduh dan Menyimpan Media dari Snapchat.\nSilakan pilih lokasi di mana media harus diunduh.", + "select_save_folder_button": "Pilih folder" + }, + "mappings": { + "dialog": "Untuk mendukung berbagai Versi Snapchat secara dinamis, pemetaan diperlukan agar SnapEnhance berfungsi dengan baik, ini tidak akan memakan waktu lebih dari 5 detik.", + "generate_button": "Menghasilkan", + "generate_failure_no_snapchat": "SnapEnhance tidak dapat mendeteksi Snapchat, coba instal ulang Snapchat.", + "generate_failure": "Terjadi kesalahan saat mencoba membuat pemetaan, silakan coba lagi.", + "generate_success": "Pemetaan berhasil dibuat." + }, + "permissions": { + "dialog": "Untuk melanjutkan, Anda harus memenuhi persyaratan berikut:", + "notification_access": "Akses Notifikasi", + "battery_optimization": "Optimasi Baterai", + "display_over_other_apps": "Tampilkan Di Atas Aplikasi Lain", + "request_button": "Meminta" + } + }, + "manager": { + "routes": { + "features": "Fitur", + "home": "Rumah", + "home_settings": "Pengaturan", + "home_logs": "Log", + "social": "Sosial", + "scripts": "Skrip" + }, + "sections": { + "home": { + "logs": { + "clear_logs_button": "Hapus Log" + } + } + } + } +} diff --git a/common/src/main/assets/lang/it_IT.json b/common/src/main/assets/lang/it_IT.json index c79b89ee6..3dc7aa91e 100644 --- a/common/src/main/assets/lang/it_IT.json +++ b/common/src/main/assets/lang/it_IT.json @@ -1,342 +1,10 @@ { - "category": { - "spying_privacy": "Spionaggio e Privacy", - "media_manager": "Gestione Multimediale", - "ui_tweaks": "UI e modifiche", - "camera": "Fotocamera", - "updates": "Aggiornamenti", - "experimental_debugging": "Sperimentale" - }, - "action": { - "clean_cache": "Svuota la Cache", - "clear_message_logger": "Cancella Registratore dei Messaggi", - "refresh_mappings": "Aggiorna Mappature", - "open_map": "Scegli la posizione sulla mappa", - "check_for_updates": "Controlla gli aggiornamenti", - "export_chat_messages": "Esporta i messaggi della chat" - }, - "property": { - "message_logger": { - "name": "Registratore dei Messaggi", - "description": "Impedisci l'eliminazione dei messaggi" - }, - "prevent_read_receipts": { - "name": "Impedisci le Ricevute di Lettura", - "description": "Impedisci a chiunque di sapere di aver aperto i propri Snap" - }, - "hide_bitmoji_presence": { - "name": "Nascondi Presenza Bitmoji", - "description": "Nascondi la presenza della tua Bitmoji dalla chat" - }, - "better_notifications": { - "name": "Migliori Notifiche", - "description": "Mostra ulteriori informazioni nelle notifiche" - }, - "notification_blacklist": { - "name": "Lista Nera delle Notifiche", - "description": "Nascondi il tipo di notifiche selezionato" - }, - "disable_metrics": { - "name": "Disabilita le Metriche", - "description": "Disabilita le metriche inviate a Snapchat" - }, - "block_ads": { - "name": "Blocca Annunci", - "description": "Blocca la visualizzazione degli annunci" - }, - "unlimited_snap_view_time": { - "name": "Tempo di Visualizzazione Illimitato degli Scatti", - "description": "Rimuove il limite di tempo per la visualizzazione degli Snap" - }, - "prevent_sending_messages": { - "name": "Impedisci L'Invio Di Messaggi", - "description": "Impedisce l'invio di determinati tipi di messaggi" - }, - "anonymous_story_view": { - "name": "Visualizzazione Anonima delle Storie", - "description": "Impedisci a chiunque di sapere che hai visualizzato la sua storia" - }, - "hide_typing_notification": { - "name": "Nascondi le Notifiche di Digitazione", - "description": "Impedisci l'invio di notifiche di digitazione" - }, - "save_folder": { - "name": "Cartella di salvataggio", - "description": "La cartella dove tutti i media sono salvati" - }, - "auto_download_options": { - "name": "Opzioni download automatico", - "description": "Seleziona quali media scaricare automaticamente" - }, - "download_options": { - "name": "Opzioni download", - "description": "Specifica il formato del percorso di salvataggio" - }, - "chat_download_context_menu": { - "name": "Menu Contestuale Download Chat", - "description": "Abilita il menu contestuale di download della chat" - }, - "gallery_media_send_override": { - "name": "Sovrascrivi Invio Media della Galleria", - "description": "Sovrascrive i media inviati dalla galleria" - }, - "auto_save_messages": { - "name": "Salvataggio Automatico Messaggi", - "description": "Seleziona che tipo di messaggi salvare automaticamente" - }, - "force_media_source_quality": { - "name": "Forza Qualità Sorgente Media", - "description": "Sovrascrive la qualità della sorgente multimediale" - }, - "download_logging": { - "name": "Scarica Log", - "description": "Mostra un notifica durante il download del media" - }, - "enable_friend_feed_menu_bar": { - "name": "Barra dei Menu Feed Amici", - "description": "Abilita la nuova Barra dei Menu Feed Amici" - }, - "friend_feed_menu_buttons": { - "name": "Pulsanti Menu Feed Amici", - "description": "Seleziona quali pulsanti mostrare nella Barra dei Menu Feed Amici" - }, - "friend_feed_menu_buttons_position": { - "name": "Indice Posizione Pulsanti Feed Amici", - "description": "La posizione dei pulsanti del menu Feed Amici" - }, - "hide_ui_elements": { - "name": "Nascondi Elementi UI", - "description": "Seleziona quali elementi UI sono da nascondere" - }, - "hide_story_section": { - "name": "Nascondi Sezione Storia", - "description": "Nascondi alcuni elementi della UI mostrati nella sezione storia" - }, - "story_viewer_override": { - "name": "Sovrascrivi Visualizzatore Storia", - "description": "Attiva alcune funzionalità che Snapchat ha nascosto" - }, - "streak_expiration_info": { - "name": "Mostra Le Informazioni Di Scadenza Streak", - "description": "Mostra le informazioni sulla scadenza della Serie, affianco alle serie" - }, - "disable_snap_splitting": { - "name": "Disabilita Divisione A Scatto", - "description": "Impedisci la divisione in più parti degli Scatti" - }, - "disable_video_length_restriction": { - "name": "Disabilita la Limitazione della Lunghezza del Video", - "description": "Disabilita le limitazioni della durata dei video" - }, - "snapchat_plus": { - "name": "Snapchat Plus", - "description": "Abilita le funzionalità di Snapchat Plus" - }, - "new_map_ui": { - "name": "Nuova UI Mappa", - "description": "Abilita l'UI della nuova mappa" - }, - "location_spoof": { - "name": "Falsificatore Posizione Snapmap", - "description": "Falsifica la tua posizione sulla Snapmap" - }, - "message_preview_length": { - "name": "Durata Anteprima del Messaggio", - "description": "Specifica la quantità di messaggi da visualizzare in anteprima" - }, - "unlimited_conversation_pinning": { - "name": "Fissazione Illimitata delle Conversazioni", - "description": "Consente di fissare conversazioni illimitate" - }, - "disable_spotlight": { - "name": "Disabilita Spotlight", - "description": "Disabilita la pagina di Spotlight" - }, - "enable_app_appearance": { - "name": "Consenti Impostazioni d'Aspetto dell'App", - "description": "Abilita le impostazioni di aspetto dell'app nascoste" - }, - "startup_page_override": { - "name": "Sovrascrivi Pagina d'Avvio", - "description": "Sovrascrive la pagina di avvio" - }, - "disable_google_play_dialogs": { - "name": "Disabilita Finestre di Google Play Services", - "description": "Impedisci la visualizzazione delle finestre di disponibilità di Google Play Services" - }, - "auto_updater": { - "name": "Aggiornamento Automatico", - "description": "L'intervallo di verifica degli aggiornamenti" - }, - "disable_camera": { - "name": "Disabilita Fotocamera", - "description": "Impedisce a Snapchat di poter utilizzare la fotocamera" - }, - "immersive_camera_preview": { - "name": "Anteprima Immersiva Fotocamera", - "description": "Impedisce a Snapchat di ritagliare l'anteprima della fotocamera" - }, - "preview_resolution": { - "name": "Risoluzione dell'Anteprima", - "description": "Sovrascrive la risoluzione d'anteprima della fotocamera" - }, - "picture_resolution": { - "name": "Risoluzione dell'Immagine", - "description": "Sovrascrive la risoluzione dell'immagine" - }, - "force_highest_frame_rate": { - "name": "Forza Frame Rate Più Elevato", - "description": "Forza la frequenza di fotogrammi maggiore possibile" - }, - "force_camera_source_encoding": { - "name": "Forza Codifica Fonte Fotocamera", - "description": "Forza la codifica della fonte della fotocamera" - }, - "app_passcode": { - "name": "Imposta Passcode dell'App", - "description": "Imposta un passcode per bloccare l'app" - }, - "app_lock_on_resume": { - "name": "Blocca App Quando Riaperta", - "description": "Blocca l'app quando è riaperta" - }, - "infinite_story_boost": { - "name": "Incremento Infinito Storie", - "description": "Incrementa infinitamente le tue storie" - }, - "meo_passcode_bypass": { - "name": "Superamento Passcode My Eyes Only", - "description": "Supera il passcode di My Eyes Only\nFunzionerà soltanto se il passcode è stato precedentemente inserito correttamente" - }, - "amoled_dark_mode": { - "name": "Modalità Scura AMOLED", - "description": "Abilita la modalità scura AMOLED\nAssicurati che la modalità scura di Snapchat sia abilitata" - }, - "unlimited_multi_snap": { - "name": "Snap Multipli Illimitati", - "description": "Ti consente di scattare una quantità illimitata di scatti multipli" - }, - "device_spoof": { - "name": "Falsifica Valori Dispositivo", - "description": "Falsifica i valori del dispositivo" - }, - "device_fingerprint": { - "name": "Impronta Digitale del Dispositivo", - "description": "Falsifica l'impronta digitale del dispositivo" - }, - "android_id": { - "name": "ID Android", - "description": "Falsifica gli ID Android dei dispositivi" - } - }, - "option": { - "property": { - "better_notifications": { - "chat": "Mostra messaggi della chat", - "snap": "Mostra media", - "reply_button": "Aggiungi pulsante di risposta", - "download_button": "Aggiungi pulsante di download" - }, - "friend_feed_menu_buttons": { - "auto_download_blacklist": "⬇️ Lista Nera Download Automatici", - "anti_auto_save": "💬 Impedisci Salvataggio Automatico Messaggi", - "stealth_mode": "👻 Modalità Stealth", - "conversation_info": "👤 Info sulla Conversazione" - }, - "download_options": { - "allow_duplicate": "Consenti download duplicati", - "create_user_folder": "Crea cartella per ogni utente", - "append_hash": "Aggiungi un hash univoco al nome del file", - "append_username": "Aggiungi il nome utente al nome del file", - "append_date_time": "Aggiungi la data e l'ora al nome del file", - "append_type": "Aggiungi il tipo di file multimediale al nome del file", - "merge_overlay": "Unisci Sovrapposizioni delle Immagini Snap" - }, - "auto_download_options": { - "friend_snaps": "Snap di Amici", - "friend_stories": "Snap Storie", - "public_stories": "Storie Pubbliche", - "spotlight": "Spotlight" - }, - "download_logging": { - "started": "Avviato", - "success": "Riuscito", - "progress": "Avanzamento", - "failure": "Fallito" - }, - "auto_save_messages": { - "NOTE": "Nota Vocale", - "CHAT": "Chat", - "EXTERNAL_MEDIA": "Media Esterno", - "SNAP": "Snap", - "STICKER": "Adesivo" - }, - "notifications": { - "chat_screenshot": "Istantanea", - "chat_screen_record": "Registrazione Schermo", - "camera_roll_save": "Salvataggo Rullino", - "chat": "Chat", - "chat_reply": "Risposta alla Chat", - "snap": "Aggancia", - "typing": "Sta scrivendo...", - "stories": "Storie", - "initiate_audio": "Chiamata Vocale in Arrivo", - "abandon_audio": "Chiamata Vocale Persa", - "initiate_video": "Videochiamata in Arrivo", - "abandon_video": "Videochiamata Persa" - }, - "gallery_media_send_override": { - "ORIGINAL": "Originale", - "NOTE": "Nota Vocale", - "SNAP": "Snap", - "LIVE_SNAP": "Scatto con audio" - }, - "hide_ui_elements": { - "remove_call_buttons": "Rimuovi Pulsanti di Chiamata", - "remove_cognac_button": "Rimuovi Pulsante Cognac", - "remove_live_location_share_button": "Rimuovi Pulsante Condividi Posizione Live", - "remove_stickers_button": "Rimuovi Pulsante Adesivi", - "remove_voice_record_button": "Rimuovi Pulsante Registrazione Vocale", - "remove_camera_borders": "Rimuovi Bordi Fotocamera" - }, - "auto_updater": { - "DISABLED": "Disabilitato", - "EVERY_LAUNCH": "A Ogni Avviio", - "DAILY": "Ogni Giorno", - "WEEKLY": "Ogni Settimana" - }, - "story_viewer_override": { - "OFF": "Spento", - "DISCOVER_PLAYBACK_SEEKBAR": "Abilita Barra di Ricerca di Riproduzione Scopri", - "VERTICAL_STORY_VIEWER": "Abilita Visualizzatore Verticale delle Storie" - }, - "hide_story_section": { - "hide_friend_suggestions": "Nascondi i suggerimenti degli amici", - "hide_friends": "Nascondi sezione degli amici", - "hide_following": "Nascondi sezione dei seguiti", - "hide_for_you": "Nascondi la sezione dei Per Te" - }, - "startup_page_override": { - "OFF": "Off", - "ngs_map_icon_container": "Mappa", - "ngs_chat_icon_container": "Chat", - "ngs_camera_icon_container": "Fotocamera", - "ngs_community_icon_container": "Community / Storie", - "ngs_spotlight_icon_container": "Riflettore", - "ngs_search_icon_container": "Cerca" - } - } - }, "friend_menu_option": { "preview": "Anteprima", "stealth_mode": "Modalità Stealth", "auto_download_blacklist": "Scarica Automaticamente la Lista Nera", "anti_auto_save": "Anti-Salvataggio Automatico" }, - "message_context_menu_option": { - "download": "Scarica", - "preview": "Anteprima" - }, "chat_action_menu": { "preview_button": "Anteprima", "download_button": "Scarica", @@ -360,26 +28,12 @@ }, "profile_info": { "title": "Info sul Profilo", - "username": "Nome Utente", "display_name": "Nome Visualizzato", "added_date": "Data di Aggiunta", "birthday": "Compleanno: {day} {month}" }, - "auto_updater": { - "no_update_available": "Nessun Aggiornamento disponibile!", - "dialog_title": "Nuovo Aggiornamento disponibile!", - "dialog_message": "Un nuovo Aggiornamento per SnapEnhance è disponibile! ({version})\n\n{body}", - "dialog_positive_button": "Scarica e Installa", - "dialog_negative_button": "Annulla", - "downloading_toast": "Scaricando l'Aggiornamento...", - "download_manager_notification_title": "Scaricando l'APK di SnapEnhance..." - }, "chat_export": { - "select_export_format": "Seleziona il Formato d'Esportazione", - "select_media_type": "Seleziona Tipi di Media da esportare", - "select_conversation": "Seleziona un Conversazione da esportare", "dialog_negative_button": "Annulla", - "dialog_neutral_button": "Esporta Tutto", "dialog_positive_button": "Esporta", "exported_to": "Esportato a {path}", "exporting_chats": "Esportando le Chat...", @@ -397,31 +51,6 @@ "cancel": "Annulla", "open": "Apri" }, - "download_manager_activity": { - "remove_all_title": "Rimuovi tutti i Download", - "remove_all_text": "Sei sicuro di volerlo fare?", - "remove_all": "Rimuovi Tutto", - "no_downloads": "Nessun download", - "cancel": "Annulla", - "file_not_found_toast": "Il file non esiste!", - "category": { - "all_category": "Tutto", - "pending_category": "In Sospeso", - "snap_category": "Scatti", - "story_category": "Storie", - "spotlight_category": "Spotlight" - }, - "debug_settings": "Impostazioni di Debug", - "debug_settings_page": { - "clear_file_title": "Cancella il file {file_name}", - "clear_file_confirmation": "Sei sicuro di voler cancellare il file {file_name}?", - "clear_cache_title": "Svuota la Cache", - "reset_all_title": "Ripristina tutte le impostazioni", - "reset_all_confirmation": "Sei sicuro di voler ripristinare tutte le impostazioni?", - "success_toast": "Successo!", - "device_spoofer": "Falsificatore Dispositivo" - } - }, "download_processor": { "download_started_toast": "Download avviato", "unsupported_content_type_toast": "Tipo di contenuto non supportato!", @@ -432,16 +61,6 @@ "download_toast": "Scaricando {path}...", "processing_toast": "Elaborando {path}...", "failed_generic_toast": "Impossibile scaricare", - "failed_to_create_preview_toast": "Impossibile creare l'anteprima", - "failed_processing_toast": "Impossibile elaborare {error}", - "failed_gallery_toast": "Impossibile salvare sulla galleria {error}" - }, - "config_activity": { - "title": "Impostazioni di SnapEnhance", - "selected_text": "{count} selezionati", - "invalid_number_toast": "Numero non valido!" - }, - "spoof_activity": { - "title": "Falsifica Impostazioni" + "failed_to_create_preview_toast": "Impossibile creare l'anteprima" } -} \ No newline at end of file +} diff --git a/common/src/main/assets/lang/ku.json b/common/src/main/assets/lang/ku.json new file mode 100644 index 000000000..0d006fd0d --- /dev/null +++ b/common/src/main/assets/lang/ku.json @@ -0,0 +1,76 @@ +{ + "setup": { + "dialogs": { + "select_language": "هەڵبژاردنی زمان", + "save_folder": "سناپ ئینهانس پێویستی بە ڕێپێدانی بیرگە هەیە بۆ دابەزاندن و سەیڤ کردنی وێنە و ڤیدیۆ لە سناپچات.\n تکایە ئەو شوێنە هەڵبژێرە کە دەتەوێت وێنە و ڤیدیۆکانی بخرێتە ناو.", + "select_save_folder_button": "هەڵبژاردنی فۆڵدەر" + }, + "mappings": { + "dialog": "بۆ ئەوەی بە شێوەیەکی داینامیکی پشتگیری لە زۆربەی وەشانەکانی سناپچات بکات، Mapping پێویستە بۆ ئەوەی SnapEnhance بە باشی کار بکات، ئەمە نابێت زیاتر لە ٥ چرکە بخایەنێت.", + "generate_button": "Hey" + } + }, + "manager": { + "sections": { + "social": { + "participants_text": "{count} بەژداربوو" + } + } + }, + "friend_menu_option": { + "preview": "بینینی پێشوەختە", + "stealth_mode": "دۆخی شاراوە", + "auto_download_blacklist": "لیستی ڕەشی دابەزاندنی ئۆتۆماتیکی", + "anti_auto_save": "دژە ئۆتۆ سەیڤ" + }, + "chat_action_menu": { + "preview_button": "بینینی پێشوەختە", + "download_button": "دابەزاندن", + "delete_logged_message_button": "سڕینەوەی پەیامی تۆمارکراو" + }, + "opera_context_menu": { + "download": "داگرتنی میدیا" + }, + "modal_option": { + "profile_info": "زانیاری پڕۆفایلی", + "close": "داخستن" + }, + "gallery_media_send_override": { + "multiple_media_toast": "لە یەک کاتدا تەنها دەتوانیت یەک میدیا بنێریت" + }, + "conversation_preview": { + "total_messages": "کۆی گشتی پەیامە نێردراوەکان/وەرگیراوەکان: {count}", + "title": "بینینی پێشوەختە", + "unknown_user": "بەکارهێنەری نەناسراو" + }, + "profile_info": { + "title": "زانیاری پڕۆفایلی", + "display_name": "ناوی پیشاندراو", + "added_date": "بەرواری زیادکراو" + }, + "chat_export": { + "dialog_negative_button": "ڕەتکردنەوە", + "dialog_positive_button": "هەناردە", + "exported_to": "هەناردە کراوە بۆ {path}", + "exporting_chats": "هەناردەکردنی چاتەکان...", + "writing_output": "نووسینی دەرئەنجام...", + "no_messages_found": "هیچ نامەیەک نەدۆزراوەتەوە!" + }, + "button": { + "ok": "باشە", + "positive": "بەڵێ", + "negative": "نەخێر", + "cancel": "ڕەتکردنەوە", + "open": "کردنه‌وه‌" + }, + "download_processor": { + "download_started_toast": "داگرتن دەستی پێکرد", + "unsupported_content_type_toast": "جۆری ناوەڕۆکی پشتگیری نەکراو!", + "failed_no_longer_available_toast": "ئەو میدیایە چیتر بەردەست نەماوە", + "already_queued_toast": "میدیا لە ئێستاوە لە ڕیزدایە!", + "already_downloaded_toast": "میدیا پێشتر دابەزێنراوە!", + "saved_toast": "پاشەکەوت کراوە بۆ {path}", + "failed_generic_toast": "شکستی هێنا لە دابەزاندن", + "failed_to_create_preview_toast": "شکستی هێنا لە دروستکردنی بینینی پێشوەختە" + } +} diff --git a/common/src/main/assets/lang/nb_NO.json b/common/src/main/assets/lang/nb_NO.json new file mode 100644 index 000000000..d2f0c7778 --- /dev/null +++ b/common/src/main/assets/lang/nb_NO.json @@ -0,0 +1,106 @@ +{ + "manager": { + "routes": { + "home_logs": "Loggføring", + "scripts": "Skript", + "home_settings": "Innstillinger", + "features": "Funksjoner", + "home": "Hjem", + "tasks": "Gjøremål" + }, + "sections": { + "social": { + "rules_title": "Regler", + "streaks_length_text": "Lengde: {length}", + "not_found": "Ikke funnet", + "participants_text": "{count} deltagere", + "reminder_button": "Sett påminnelse" + }, + "home": { + "logs": { + "clear_logs_button": "Tøm loggføring", + "export_logs_button": "Eksporter loggføring" + } + }, + "features": { + "disabled": "Avskrudd" + } + }, + "dialogs": { + "add_friend": { + "search_hint": "Søk", + "category_friends": "Venner", + "category_groups": "Grupper" + }, + "scripting_warning": { + "title": "Advarsel" + } + } + }, + "setup": { + "mappings": { + "generate_button": "Generer" + }, + "dialogs": { + "select_save_folder_button": "Velg mappe", + "select_language": "Velg språk" + }, + "permissions": { + "battery_optimization": "Batterioptimalisering", + "display_over_other_apps": "Vis over andre programmer", + "notification_access": "Merknadstilgang", + "request_button": "Forespør" + } + }, + "rules": { + "modes": { + "blacklist": "Svartelistingsmodus", + "whitelist": "Hvitlistingsmodus" + }, + "properties": { + "auto_save": { + "options": { + "whitelist": "Automatisk lagring" + }, + "name": "Automatisk lagring" + } + } + }, + "features": { + "properties": { + "downloader": { + "properties": { + "ffmpeg_options": { + "properties": { + "video_bitrate": { + "name": "Videobitrate", + "description": "Sett videobitraten (kbps)" + }, + "threads": { + "name": "Tråder" + }, + "audio_bitrate": { + "name": "Lydbitrate", + "description": "Sett lydbitraten (kbps)" + }, + "custom_video_codec": { + "name": "Egendefinert videokodek" + } + } + } + } + }, + "user_interface": { + "properties": { + "bootstrap_override": { + "properties": { + "home_tab": { + "name": "Hjemmefane" + } + } + } + } + } + } + } +} diff --git a/common/src/main/assets/lang/nl.json b/common/src/main/assets/lang/nl.json new file mode 100644 index 000000000..56d7eee41 --- /dev/null +++ b/common/src/main/assets/lang/nl.json @@ -0,0 +1,247 @@ +{ + "setup": { + "dialogs": { + "select_language": "Kies taal", + "save_folder": "SnapEnhance heeft toegang voor opslag nodig om media van Snapchat te downloaden.\nKies de downloadlocatie.", + "select_save_folder_button": "Selecteer map" + }, + "mappings": { + "dialog": "Om een breed bereik aan Snapchat-versies te ondersteunen, zijn er toewijzingen nodig om SnapEnhance goed te laten werken. Dit zou niet meer dan 5 seconden mogen duren.", + "generate_button": "Genereren", + "generate_failure_no_snapchat": "SnapEnhance kon de Snapchat niet detecteren, probeer alstublieft Snapchat opnieuw te installeren.", + "generate_failure": "Er is een fout opgetreden tijdens het genereren van mappings, probeer het opnieuw.", + "generate_success": "Mappings succesvol gegenereerd." + }, + "permissions": { + "dialog": "Om door te gaan moet je voldoen aan de volgende vereisten:", + "notification_access": "Toegang tot meldingen", + "battery_optimization": "Batterijoptimalisatie", + "display_over_other_apps": "Weergeven vóór andere apps", + "request_button": "Verzoek" + } + }, + "manager": { + "routes": { + "features": "Functies", + "home": "Startscherm", + "home_settings": "Instellingen", + "home_logs": "Logs", + "social": "Sociaal", + "scripts": "Scripts" + }, + "sections": { + "home": { + "logs": { + "clear_logs_button": "Wis logs", + "export_logs_button": "Logs exporteren" + } + }, + "features": { + "disabled": "Uitgeschakeld" + }, + "social": { + "e2ee_title": "End-to-End encryptie", + "rules_title": "Regels", + "participants_text": "{count} deelnemers", + "not_found": "Niet gevonden", + "streaks_title": "Snapreeks", + "streaks_length_text": "Lengte: {length}", + "streaks_expiration_short": "{hours}u", + "streaks_expiration_text": "Verloopt in {eta}", + "reminder_button": "Stel herinnering in" + } + }, + "dialogs": { + "add_friend": { + "title": "Vriend of groep toevoegen", + "search_hint": "Zoeken", + "fetch_error": "Kan gegevens niet ophalen", + "category_groups": "Groepen", + "category_friends": "Vrienden" + } + } + }, + "rules": { + "modes": { + "blacklist": "Zwarte lijst modus", + "whitelist": "Witte lijst modus" + }, + "properties": { + "auto_download": { + "name": "Automatisch downloaden", + "description": "Download Snaps automatisch tijdens het bekijken", + "options": { + "blacklist": "Uitsluiten van automatisch downloaden", + "whitelist": "Automatische download" + } + }, + "stealth": { + "name": "Verberg Modus", + "description": "Voorkomt dat iedereen weet dat je hun snaps/chats en gesprekken hebt geopend", + "options": { + "blacklist": "Uitsluiten van Verberg Modus", + "whitelist": "Verberg modus" + } + }, + "auto_save": { + "name": "Automatisch opslaan", + "description": "Slaat chatberichten op wanneer je ze bekijkt", + "options": { + "blacklist": "Uitsluiten van automatisch opslaan", + "whitelist": "Automatisch opslaan" + } + }, + "hide_friend_feed": { + "name": "Verbergen in Vrienden Feed" + }, + "e2e_encryption": { + "name": "Gebruik E2E Encryptie" + }, + "pin_conversation": { + "name": "Gesprek vastzetten" + } + } + }, + "actions": { + "clean_snapchat_cache": "Snapchat cache opschonen", + "clear_message_logger": "Wis Berichten Logger", + "refresh_mappings": "Vernieuw Mappings", + "open_map": "Kies locatie op de kaart", + "check_for_updates": "Controleer op updates", + "export_chat_messages": "Exporteer chatberichten" + }, + "features": { + "notices": { + "unstable": "⚠️ Onstabiel", + "ban_risk": "⚠️ Deze functie kan bans veroorzaken", + "internal_behavior": "⚠️ Dit kan het interne gedrag van Snapchat breken", + "require_native_hooks": "⚠️ Deze functie vereist experimentele Native Hooks om correct te werken" + }, + "properties": { + "downloader": { + "name": "Downloader", + "properties": { + "save_folder": { + "name": "Opslag locatie", + "description": "Selecteer de map waar alle media naar moeten worden gedownload" + }, + "auto_download_sources": { + "name": "Automatisch Bronnen downloaden", + "description": "Selecteer de bronnen waaruit je automatisch wilt downloaden" + }, + "prevent_self_auto_download": { + "name": "Eigen automatische download voorkomen", + "description": "Voorkomt dat je eigen snaps automatisch worden gedownload" + }, + "path_format": { + "name": "Pad Formaat", + "description": "Geef het bestandspad formaat op" + }, + "allow_duplicate": { + "name": "Duplicaten toestaan", + "description": "Staat toe om meerdere keren hetzelfde media te downloaden" + }, + "merge_overlays": { + "name": "Overlays Samenvoegen", + "description": "Combineert de tekst en de media van een Snap in een enkel bestand" + }, + "force_image_format": { + "name": "Forceer Afbeeldingsformaat", + "description": "Forceert dat afbeeldingen opgeslagen moeten worden in een gespecificeerd formaat" + }, + "force_voice_note_format": { + "name": "Forceer spraaknotitie formaat", + "description": "Forceert dat spraaknotities opgeslagen moeten worden in een opgegeven formaat" + }, + "download_profile_pictures": { + "name": "Download profielfoto's", + "description": "Maakt het mogelijk om profielfoto's van de profielpagina te downloaden" + }, + "chat_download_context_menu": { + "name": "Chat Download Context Menu", + "description": "Maakt het mogelijk om media van een gesprek te downloaden door deze lang ingedrukt te houden" + }, + "ffmpeg_options": { + "name": "FFmpeg Opties", + "description": "Specificeer extra FFmpeg opties", + "properties": { + "threads": { + "name": "Threads", + "description": "Het aantal threads dat moet worden gebruikt" + }, + "preset": { + "name": "Voorinstelling", + "description": "Stel de snelheid van de conversie in" + } + } + } + } + }, + "user_interface": { + "description": "Verander het uiterlijk en gevoel van Snapchat", + "properties": { + "enable_app_appearance": { + "name": "Activeer App Uiterlijk Instellingen", + "description": "Schakelt de verborgen App Uiterlijk Instelling in\nMogelijk niet nodig in nieuwere Snapchat versies" + }, + "amoled_dark_mode": { + "name": "AMOLED Donkere Modus", + "description": "Schakelt AMOLED donkere modus in\nZorg ervoor dat Snapchats Donkere modus is ingeschakeld" + }, + "friend_feed_message_preview": { + "name": "Voorbeeld Vrienden Feed Bericht", + "description": "Toont een voorbeeld van de laatste berichten in de Vrienden Feed", + "properties": { + "amount": { + "name": "Hoeveelheid", + "description": "Het aantal berichten om een voorbeeld te krijgen" + } + } + }, + "bootstrap_override": { + "name": "Bootstrap overschrijven", + "description": "Overschrijft de gebruikersinterface bootstrap instellingen", + "properties": { + "app_appearance": { + "name": "App Uiterlijk", + "description": "Stelt een persistent App uiterlijk in" + }, + "home_tab": { + "name": "Startpagina tabblad", + "description": "Overschrijft het starttabblad bij het openen van Snapchat" + } + } + }, + "map_friend_nametags": { + "name": "Verbeterde Kaart Naamtags Van Vrienden", + "description": "Verbetert de naamtags van vrienden op de Snapmap" + }, + "streak_expiration_info": { + "name": "Toon Snapreeks vervaldatum info", + "description": "Toont een Snapreeks Verlooptimer naast de Snapreeks teller" + }, + "hide_friend_feed_entry": { + "name": "Verberg Vrienden Feed", + "description": "Verbergt een specifieke vriend uit de Vrienden Feed\nGebruik het sociale tabblad om deze functie te beheren" + }, + "hide_streak_restore": { + "name": "Snapreeksherstel verbergen", + "description": "Verbergt de Herstel knop in de vrienden feed" + }, + "hide_story_sections": { + "name": "Verhalen Sectie verbergen", + "description": "Verberg bepaalde UI-elementen die worden weergegeven in het verhaalgedeelte" + }, + "hide_ui_components": { + "name": "Verberg UI Componenten", + "description": "Selecteer welke UI componenten te verbergen" + }, + "disable_spotlight": { + "name": "Spotlight uitschakelen", + "description": "Schakelt de Spotlight pagina uit" + } + } + } + } + } +} diff --git a/common/src/main/assets/lang/pl.json b/common/src/main/assets/lang/pl.json new file mode 100644 index 000000000..cca127951 --- /dev/null +++ b/common/src/main/assets/lang/pl.json @@ -0,0 +1,112 @@ +{ + "setup": { + "dialogs": { + "select_language": "Wybierz język", + "save_folder": "SnapEnhance wymaga uprawnień do przechowywania danych w celu pobierania i zapisywania mediów z Snapchat.\nProszę wybrać lokalizację, do której media powinny być pobierane.", + "select_save_folder_button": "Wybierz Folder" + }, + "mappings": { + "dialog": "Aby dynamicznie obsługiwać szeroki zakres wersji Snapchat, konieczne są mapowania, aby SnapEnhance działał poprawnie. To nie powinno zająć więcej niż 5 sekund.", + "generate_button": "Wygeneruj", + "generate_failure_no_snapchat": "SnapEnhance nie było w stanie wykryć Snapchata, spróbuj ponownie zainstalować Snapchat.", + "generate_failure": "Wystąpił błąd podczas próby generowania mapowań, spróbuj ponownie.", + "generate_success": "Mapowania zostały pomyślnie wygenerowane." + }, + "permissions": { + "dialog": "Aby kontynuować, musisz spełnić poniższe wymagania:", + "notification_access": "Dostęp do powiadomień", + "battery_optimization": "Optymalizacja baterii", + "display_over_other_apps": "Wyświetlanie nad innymi aplikacjami", + "request_button": "Wymagania" + } + }, + "manager": { + "routes": { + "features": "Cechy", + "home": "Strona główna", + "home_settings": "Ustawienia", + "home_logs": "Logi", + "social": "Społeczność", + "scripts": "Skrypty" + }, + "sections": { + "home": { + "logs": { + "clear_logs_button": "Wyczyść logi", + "export_logs_button": "Eksportuj logi" + } + }, + "features": { + "disabled": "Wyłączony" + }, + "social": { + "e2ee_title": "Szyfrowanie end-to-end", + "rules_title": "Reguły" + } + } + }, + "friend_menu_option": { + "preview": "Podgląd", + "stealth_mode": "Tryb Stealth", + "auto_download_blacklist": "Automatyczne pobieranie czarnej listy", + "anti_auto_save": "Anty-automatyczne zapisywanie" + }, + "chat_action_menu": { + "preview_button": "Podgląd", + "download_button": "Pobierz", + "delete_logged_message_button": "Usuń zarejestrowaną wiadomość" + }, + "opera_context_menu": { + "download": "Pobierz media" + }, + "modal_option": { + "profile_info": "Info o profilu", + "close": "Zamknij" + }, + "gallery_media_send_override": { + "multiple_media_toast": "Możesz wysłać tylko jeden nośnik na raz" + }, + "conversation_preview": { + "streak_expiration": "wygasa za {day} dni {hour} godziny {minute} minuty", + "total_messages": "Łączna liczba wysłanych/odebranych wiadomości: {count}", + "title": "Podgląd", + "unknown_user": "Nieznany użytkownik" + }, + "profile_info": { + "title": "Info o profilu", + "display_name": "Wyświetl nazwę", + "added_date": "Dodaj datę", + "birthday": "Urodziny : {month} {day}" + }, + "chat_export": { + "dialog_negative_button": "Anuluj", + "dialog_positive_button": "Eksportuj", + "exported_to": "Wyeksportowano do {path}", + "exporting_chats": "Eksportowanie czatów...", + "processing_chats": "Przetwarzanie rozmów {amount}...", + "export_fail": "Nie udało się wyeksportować rozmowy {conversation}", + "writing_output": "Zapisywanie danych...", + "finished": "Gotowe! Teraz możesz zamknąć to okno.", + "no_messages_found": "Nie znaleziono wiadomości!", + "exporting_message": "Eksportowanie {conversation}..." + }, + "button": { + "ok": "Ok", + "positive": "Tak", + "negative": "Nie", + "cancel": "Anuluj", + "open": "Otwórz" + }, + "download_processor": { + "download_started_toast": "Rozpoczęto pobieranie", + "unsupported_content_type_toast": "Nieobsługiwany typ treści!", + "failed_no_longer_available_toast": "Media już niedostępne", + "already_queued_toast": "Media są już w kolejce!", + "already_downloaded_toast": "Media już pobrane!", + "saved_toast": "Zapisano w {path}", + "download_toast": "Pobieranie {path}...", + "processing_toast": "Przetwarzanie {path}...", + "failed_generic_toast": "Pobieranie nie powiodło się", + "failed_to_create_preview_toast": "Nie można wczytać podglądu" + } +} diff --git a/common/src/main/assets/lang/pt.json b/common/src/main/assets/lang/pt.json new file mode 100644 index 000000000..878b64c29 --- /dev/null +++ b/common/src/main/assets/lang/pt.json @@ -0,0 +1,38 @@ +{ + "chat_action_menu": { + "preview_button": "Pré-visualização", + "download_button": "Descarregar" + }, + "modal_option": { + "profile_info": "Informações do Perfil", + "close": "Fechar" + }, + "conversation_preview": { + "streak_expiration": "expira em {day} dias {hour} horas {minute} minutos", + "title": "Pré-visualização", + "unknown_user": "Usuário Desconhecido" + }, + "profile_info": { + "title": "Informações do Perfil", + "display_name": "Nome de Exibição", + "birthday": "Aniversário: {month} {day}" + }, + "chat_export": { + "dialog_negative_button": "Cancelar", + "dialog_positive_button": "Exportar", + "exported_to": "Exportado para {path}", + "exporting_chats": "A Exportar Conversas...", + "processing_chats": "A Processar {amount} conversas...", + "export_fail": "Falha ao exportar a conversa {conversation}", + "finished": "Pronto! Já pode fechar este diálogo.", + "no_messages_found": "Nenhuma mensagem foi encontrada!", + "exporting_message": "A Exportar {conversation}..." + }, + "button": { + "ok": "Aceitar", + "positive": "Sim", + "negative": "Não", + "cancel": "Cancelar", + "open": "Abrir" + } +} diff --git a/common/src/main/assets/lang/ro.json b/common/src/main/assets/lang/ro.json new file mode 100644 index 000000000..de9c9761d --- /dev/null +++ b/common/src/main/assets/lang/ro.json @@ -0,0 +1,53 @@ +{ + "friend_menu_option": { + "preview": "Previzualizare", + "stealth_mode": "Modul Mascare", + "anti_auto_save": "Anti Salvare Automată" + }, + "modal_option": { + "profile_info": "Informații Profil", + "close": "Închide" + }, + "gallery_media_send_override": { + "multiple_media_toast": "Puteți trimite un singur fișier media la un moment dat" + }, + "conversation_preview": { + "streak_expiration": "expiră în {day} zile {hour} ore {minute} minute", + "total_messages": "Total mesaje trimise/primite: {count}", + "title": "Previzualizare", + "unknown_user": "Utilizator Necunoscut" + }, + "profile_info": { + "display_name": "Nume afișat" + }, + "chat_export": { + "dialog_negative_button": "Anulare", + "exported_to": "Exportat către {path}", + "exporting_chats": "Se exportă conversațiile...", + "processing_chats": "Se procesează {amount} de conversații...", + "export_fail": "Nu s-a putut exporta conversația {conversation}", + "writing_output": "Se scrie rezultatul...", + "finished": "Gata! Acum puteți să închideți acest dialog.", + "no_messages_found": "Niciun mesaj găsit!", + "exporting_message": "Se exportă {conversation}..." + }, + "button": { + "ok": "OK", + "positive": "Da", + "negative": "Nu", + "cancel": "Anulare", + "open": "Deschide" + }, + "download_processor": { + "download_started_toast": "Descărcarea a început", + "unsupported_content_type_toast": "Tip de conținut nesuportat!", + "failed_no_longer_available_toast": "Media nu mai e valabilă", + "already_queued_toast": "Media este deja în așteptare!", + "already_downloaded_toast": "Media deja descărcată!", + "saved_toast": "Salvat în {path}", + "download_toast": "Se descarcă {path}...", + "processing_toast": "Se procesează {path}...", + "failed_generic_toast": "Descărcarea a eșuat", + "failed_to_create_preview_toast": "Nu a reușit să se creeze previzualizarea" + } +} diff --git a/common/src/main/assets/lang/ru.json b/common/src/main/assets/lang/ru.json new file mode 100644 index 000000000..e9a93795f --- /dev/null +++ b/common/src/main/assets/lang/ru.json @@ -0,0 +1,16 @@ +{ + "setup": { + "dialogs": { + "select_language": "Выберите язык", + "save_folder": "SnapEnhance требует разрешения хранилища для загрузки и сохранения медиафайлов из Snapchat.\nВыберите место, куда следует загружать медиафайлы.", + "select_save_folder_button": "Выберите папку" + }, + "mappings": { + "dialog": "Для динамической поддержки широкого диапазона версий Snapchat, для корректной работы SnapEnhance необходимо сопоставление параметров, это не должно занять более 5 секунд.", + "generate_button": "Генерировать", + "generate_failure_no_snapchat": "SnapEnhance не удалось обнаружить Snapchat, попробуйте переустановить Snapchat.", + "generate_failure": "Произошла ошибка при генерации сопоставлений, пожалуйста, попробуйте еще раз.", + "generate_success": "Сопоставления сгенерированы успешно." + } + } +} diff --git a/common/src/main/assets/lang/sv.json b/common/src/main/assets/lang/sv.json new file mode 100644 index 000000000..07bf2e6c3 --- /dev/null +++ b/common/src/main/assets/lang/sv.json @@ -0,0 +1,50 @@ +{ + "setup": { + "dialogs": { + "select_save_folder_button": "Välj Mapp" + }, + "mappings": { + "dialog": "För att dynamiskt stödja ett brett utbud av Snapchat Versioner, mappningar är nödvändiga för att SnapEnhance ska fungera korrekt, detta bör inte ta mer än 5 sekunder.", + "generate_button": "Generera", + "generate_failure_no_snapchat": "SnapEnhance kunde inte upptäcka Snapchat, försök att installera om Snapchat.", + "generate_failure": "Ett fel inträffade vid försök att generera mappningar, försök igen.", + "generate_success": "Mappningar har skapats." + }, + "permissions": { + "dialog": "För att fortsätta måste du uppfylla följande krav:", + "notification_access": "Åtkomst till aviseringar", + "battery_optimization": "Batterioptimering", + "display_over_other_apps": "Visa över andra appar", + "request_button": "Begäran" + } + }, + "manager": { + "routes": { + "features": "Funktioner", + "home": "Hem", + "home_settings": "Inställningar", + "home_logs": "Loggar", + "social": "Socialt", + "scripts": "Skript" + }, + "sections": { + "home": { + "logs": { + "clear_logs_button": "Rensa loggar", + "export_logs_button": "Exportera loggar" + } + }, + "features": { + "disabled": "Inaktiverad" + }, + "social": { + "e2ee_title": "End-to-end-kryptering", + "rules_title": "Regler", + "participants_text": "{count} deltagare", + "not_found": "Hittades inte", + "streaks_title": "Streaks", + "streaks_length_text": "Längd: {length}" + } + } + } +} diff --git a/common/src/main/assets/lang/tr_TR.json b/common/src/main/assets/lang/tr_TR.json index 3da7913bd..46cfb4020 100644 --- a/common/src/main/assets/lang/tr_TR.json +++ b/common/src/main/assets/lang/tr_TR.json @@ -1,285 +1,784 @@ { - "category": { - "spying_privacy": "Casusluk ve Gizlilik", - "media_manager": "Medya Yöneticisi", - "ui_tweaks": "Arayüz ve İnce Ayarlar", - "camera": "Kamera", - "updates": "Güncellemeler", - "experimental_debugging": "Deneysel" - }, - "action": { - "clean_cache": "Temiz Önbellek", - "clear_message_logger": "Mesaj Kaydediciyi Temizle", - "refresh_mappings": "Haritalamaları Yenile", - "open_map": "Harita üzerinde konum seçin", - "check_for_updates": "Güncellemeleri kontrol edin", - "export_chat_messages": "Sohbet mesajlarını dışa aktar" - }, - "property": { - "message_logger": { - "name": "Mesaj Kaydedici", - "description": "Mesajların silinmesini önler" - }, - "prevent_read_receipts": { - "name": "Okundu Bilgisini Önle", - "description": "Herhangi birinin Snap'lerini açtığınızı bilmesini önleyin" - }, - "hide_bitmoji_presence": { - "name": "Bitmoji'yi Gizle", - "description": "Bitmoji'nizi sohbetten gizler" - }, - "better_notifications": { - "name": "Daha İyi Bildirimler", - "description": "Bildirimlerde daha fazla bilgi gösterir" - }, - "notification_blacklist": { - "name": "Bildirim Kara Listesi", - "description": "Seçilen bildirimin türünü gizler" - }, - "disable_metrics": { - "name": "Ölçümleri Devre Dışı Bırak", - "description": "Snapchat'e gönderilen ölçüm verilerini devre dışı bırakır" - }, - "block_ads": { - "name": "Reklamları Engelle", - "description": "Reklamların görüntülenmesini engeller" - }, - "unlimited_snap_view_time": { - "name": "Sınırsız Snap Görüntüleme Süresi", - "description": "Snap'leri görüntülemek için zaman sınırını kaldırır" - }, - "prevent_sending_messages": { - "name": "Mesaj Göndermeyi Engelle", - "description": "Belirli mesaj türlerinin gönderilmesini engeller" - }, - "anonymous_story_view": { - "name": "Anonim Hikaye Görünümü", - "description": "Herhangi birinin hikayelerini gördüğünüzü bilmesini engeller" - }, - "hide_typing_notification": { - "name": "Yazıyor Bildirimini Gizle", - "description": "Yazma bildirimlerinin gönderilmesini önler" - }, - "save_folder": { - "name": "Kayıt Klasörü", - "description": "Tüm medyanın kaydedildiği klasör" - }, - "auto_download_options": { - "name": "Otomatik İndirme Seçenekleri", - "description": "Hangi medyaların otomatik indirileceğini seçin" - }, - "download_options": { - "name": "İndirme Seçenekleri", - "description": "Dosya yolu biçimini belirleyin" - }, - "chat_download_context_menu": { - "name": "Sohbet İndirme İçerik Menüsü", - "description": "Sohbet indirme içerik menüsünü etkinleştirin" - }, - "gallery_media_send_override": { - "name": "Galeri Medya Yüklemesini Aç", - "description": "Galeriden medya yüklenmesine olanak verir" - }, - "auto_save_messages": { - "name": "Mesajları Otomatik Kaydet", - "description": "Hangi tür mesajların otomatik olarak kaydedileceğini seçin" - }, - "force_media_source_quality": { - "name": "Medya Kaynak Kalitesini Zorla", - "description": "Medya kaynağı kalitesini düşürmez" - }, - "download_logging": { - "name": "İndirme Günlüğü", - "description": "Medya indirilirken bir tost mesajı göster" - }, - "enable_friend_feed_menu_bar": { - "name": "Arkadaş Akışı Menü Çubuğu", - "description": "Yeni Arkadaş Akışı Menü Çubuğunu etkinleştirir" - }, - "friend_feed_menu_buttons": { - "name": "Arkadaş Akışı Menü Düğmeleri", - "description": "Arkadaş Akışı Menü Çubuğunda hangi düğmelerin gösterileceğini seçin" - }, - "friend_feed_menu_buttons_position": { - "name": "Arkadaş Akışı Düğmeleri Pozisyonu Dizini", - "description": "Arkadaş Akışı Menü Düğmelerinin konumu" - }, - "hide_ui_elements": { - "name": "Arayüz Öğelerini Gizle", - "description": "Hangi arayüz öğelerinin gizleneceğini seçin" - }, - "hide_story_section": { - "name": "Hikaye Bölümünü Gizle", - "description": "Hikaye bölümünde gösterilen belirli arayüz öğelerini gizle" - }, - "story_viewer_override": { - "name": "Hikaye Görüntüleyici Geçersiz Kılma", - "description": "Snapchat'in gizlediği belirli özellikleri açar" - }, - "streak_expiration_info": { - "name": "Seri Sona Erme Bilgisini Göster", - "description": "Serilerin yanında Seri sona erme bilgisini gösterir" - }, - "disable_snap_splitting": { - "name": "Snap Bölmeyi Devre Dışı Bırak", - "description": "Snap'lerin birden fazla parçaya bölünmesini önler" + "setup": { + "dialogs": { + "select_language": "Dil Seç", + "save_folder": "SnapEnhance, Snapchat'ten medya indirmek ve kaydetmek için depolama izinleri gerektirir.\nLütfen medyanın indirileceği konumu seçin.", + "select_save_folder_button": "Klasör Seç" }, - "disable_video_length_restriction": { - "name": "Video Uzunluğu Kısıtlamasını Devre Dışı Bırak", - "description": "Video uzunluğu kısıtlamalarını devre dışı bırakır" + "mappings": { + "dialog": "Birden çok Snapchat sürümlerini dinamik olarak desteklemek için ve SnapEnhance'in düzgün çalışması için eşlemeler gereklidir, bu işlem genellikle 5 saniyeden fazla sürmez.", + "generate_button": "Oluştur", + "generate_failure_no_snapchat": "SnapEnhance Snapchat'i algılayamadı, lütfen Snapchat'i yeniden yüklemeyi deneyin.", + "generate_failure": "Eşlemeleri oluşturmaya çalışırken bir hata oluştu, lütfen tekrar deneyin.", + "generate_success": "Eşlemeler başarıyla oluşturuldu." }, - "snapchat_plus": { - "name": "Snapchat Plus", - "description": "Snapchat Plus özelliklerini etkinleştirir" - }, - "new_map_ui": { - "name": "Yeni Harita Arayüzü", - "description": "Yeni harita arayüzünü etkinleştirir" - }, - "location_spoof": { - "name": "Snapmap Konum Değiştirici", - "description": "Snapmap'te konumunuzu taklit eder" - }, - "message_preview_length": { - "name": "Mesaj Önizleme Uzunluğu", - "description": "Önizlenecek mesaj miktarını belirtir" - }, - "unlimited_conversation_pinning": { - "name": "Sınırsız Konuşma Sabitleme", - "description": "Sınırsız konuşmayı sabitleme olanağı sağlar" - }, - "disable_spotlight": { - "name": "Spotlight'ı Devre Dışı Bırak", - "description": "Spotlight sayfasını devre dışı bırakır" - }, - "enable_app_appearance": { - "name": "Uygulama Görünüm Ayarlarını Etkinleştir", - "description": "Gizli uygulama görünümü ayarlarını etkinleştirir" - }, - "startup_page_override": { - "name": "Başlangıç Sayfasını Geçersiz Kıl", - "description": "Başlangıç sayfasını geçersiz kılar" - }, - "disable_google_play_dialogs": { - "name": "Google Play Hizmetleri İletişim Kutularını Devre Dışı Bırak", - "description": "Google Play Hizmetleri kullanılabilirlik iletişim kutularının gösterilmesini önler" - }, - "auto_updater": { - "name": "Otomatik Güncelleyici", - "description": "Güncellemeleri kontrol etme aralığı" - }, - "disable_camera": { - "name": "Kamerayı Devre Dışı Bırak", - "description": "Snapchat'in kamerayı kullanabilmesini engeller" - }, - "immersive_camera_preview": { - "name": "Sürükleyici Kamera Önizlemesi", - "description": "Snapchat'in kamera önizlemesini kırpmasını engeller" - }, - "preview_resolution": { - "name": "Önizleme Çözünürlüğü", - "description": "Kamera önizleme çözünürlüğünü değiştirir" - }, - "picture_resolution": { - "name": "Fotoğraf Çözünürlüğü", - "description": "Fotoğraf çözünürlüğünü değiştirir" - }, - "force_highest_frame_rate": { - "name": "En Yüksek Kare Hızını Zorla", - "description": "Mümkün olan en yüksek kare hızını zorlar" - }, - "force_camera_source_encoding": { - "name": "Kamera Kaynağı Kodlamasını Zorla", - "description": "Kamera kaynak kodlamasını zorlar" - }, - "app_passcode": { - "name": "Uygulama Parolasını Ayarla", - "description": "Uygulamayı kilitlemek için bir parola ayarlar" - }, - "app_lock_on_resume": { - "name": "Uygulama Yeniden Açıldığında Kilit", - "description": "Uygulama yeniden açıldığında kilitlenir" - }, - "infinite_story_boost": { - "name": "Sınırsız Hikaye Desteği", - "description": "Hikayenizi sınırsız güçlendirir" - }, - "meo_passcode_bypass": { - "name": "My Eyes Only Şifresini Kır", - "description": "My Eyes Only şifresini atlayın\nBu yalnızca parola daha önce doğru girilmişse çalışacaktır" - }, - "amoled_dark_mode": { - "name": "AMOLED Karanlık Mod", - "description": "AMOLED karanlık modunu etkinleştirir\nSnapchat'in karanlık modunun etkin olduğundan emin olun" - }, - "unlimited_multi_snap": { - "name": "Sınırsız Çoklu Snap", - "description": "Sınırsız sayıda çoklu snap çekmenizi sağlar" + "permissions": { + "dialog": "Devam etmek için aşağıdaki gerekliliklere uymanız gerekir:", + "notification_access": "Bildirim Erişimi", + "battery_optimization": "Pil Optimizasyonu", + "display_over_other_apps": "Diğer Uygulamaların Üzerinde Göster", + "request_button": "İstek" + } + }, + "manager": { + "routes": { + "features": "Özellikler", + "home": "Ana Sayfa", + "home_settings": "Ayarlar", + "home_logs": "Kayıtlar", + "social": "Sosyal", + "scripts": "Scriptler", + "tasks": "Görevler" }, - "device_spoof": { - "name": "Sahte Cihaz Değerleri", - "description": "Cihaz değerlerini taklit eder" + "sections": { + "home": { + "logs": { + "clear_logs_button": "Kayıtları Temizle", + "export_logs_button": "Kayırları Dışa Aktar" + } + }, + "features": { + "disabled": "Devre Dışı" + }, + "social": { + "e2ee_title": "Uçtan Uca Şifreleme", + "rules_title": "Kurallar", + "participants_text": "{count} Katılımcı", + "not_found": "Bulunamadı", + "streaks_title": "Seriler", + "streaks_length_text": "Uzunluk: {length}", + "streaks_expiration_short": "{hours} saat", + "streaks_expiration_text": "{eta} içerisinde sona eriyor", + "reminder_button": "Hatırlatıcıyı Ayarla" + }, + "tasks": { + "no_tasks": "Görev yok" + } }, - "device_fingerprint": { - "name": "Cihaz Parmak İzi", - "description": "Cihaz parmak izini taklit eder" + "dialogs": { + "add_friend": { + "title": "Arkadaş veya Grup Ekle", + "search_hint": "Ara", + "fetch_error": "Veri alınamadı", + "category_groups": "Gruplar", + "category_friends": "Arkadaşlar" + }, + "scripting_warning": { + "content": "SnapEnhance, cihazınızda kullanıcı tanımlı kodun yürütülmesine izin veren bir komut dosyası aracı içerir. Çok dikkatli olun ve modülleri yalnızca bilinen, güvenilir kaynaklardan yükleyin. Yetkisiz veya doğrulanmamış modüller sisteminiz için güvenlik riskleri oluşturabilir.", + "title": "Uyarı" + } + } + }, + "rules": { + "modes": { + "blacklist": "Kara liste modu", + "whitelist": "Beyaz liste modu" }, - "android_id": { - "name": "Android Kimliği", - "description": "Cihazların Android kimliğini taklit eder" + "properties": { + "auto_download": { + "name": "Otomatik İndirme", + "description": "Snap'leri görüntülerken otomatik olarak indir", + "options": { + "blacklist": "Otomatik İndirmelerden Hariç Tut", + "whitelist": "Otomatik İndirme" + } + }, + "stealth": { + "name": "Gizli Mod", + "description": "Herhangi birinin Snap'lerini/Sohbetlerini ve konuşmalarını açtığınızı bilmesini engeller", + "options": { + "blacklist": "Gizli Moddan Hariç Tut", + "whitelist": "Gizli mod" + } + }, + "auto_save": { + "name": "Otomatik Kaydet", + "description": "Sohbet mesajlarını görüntülerken kaydeder", + "options": { + "blacklist": "Otomatik kaydetmelerden hariç tut", + "whitelist": "Otomatik kaydetme" + } + }, + "hide_friend_feed": { + "name": "Arkadaş Akışından Gizle" + }, + "e2e_encryption": { + "name": "E2E Şifreleme Kullanın" + }, + "pin_conversation": { + "name": "Konuşmayı Sabitle" + }, + "unsaveable_messages": { + "name": "Kaydedilemeyen Mesajlar", + "options": { + "blacklist": "Kaydedilemeyen Mesajlardan Hariç Tut", + "whitelist": "Kaydedilemeyen Mesajlar" + }, + "description": "Mesajların diğer kişiler tarafından sohbete kaydedilmesini önler" + } } }, - "option": { - "property": { + "actions": { + "clean_snapchat_cache": "Snapchat Önbelleğini Temizle", + "clear_message_logger": "Mesaj Kaydediciyi Temizle", + "refresh_mappings": "Eşlemeleri Yenile", + "open_map": "Harita üzerinde konum seçin", + "check_for_updates": "Güncellemeleri denetle", + "export_chat_messages": "Sohbet Mesajlarını Dışa Aktar", + "bulk_messaging_action": "Toplu Mesajlaşma Eylemi", + "export_memories": "Anıları Dışa Aktar" + }, + "features": { + "notices": { + "unstable": "⚠ Stabil Değil", + "ban_risk": "⚠ Bu özellik banlanmanıza neden olabilir", + "internal_behavior": "⚠ Bu Snapchat'in dahili davranışını bozabilir", + "require_native_hooks": "⚠ Bu özelliğin düzgün çalışması için deneysel enjekte gerekir" + }, + "properties": { + "downloader": { + "name": "İndirici", + "description": "Snapchat Media'yı İndirin", + "properties": { + "save_folder": { + "name": "Kayıt Klasörü", + "description": "Tüm medyanın indirileceği dizini seçin" + }, + "auto_download_sources": { + "name": "Otomatik İndirme Kaynakları", + "description": "Otomatik olarak indirilecek kaynakları seçin" + }, + "prevent_self_auto_download": { + "name": "Kendi Kendine Otomatik İndirmeyi Önle", + "description": "Kendi Snap'lerinizin otomatik olarak indirilmesini önler" + }, + "path_format": { + "name": "Yol Formatı", + "description": "Dosya Yolu Formatını Belirleme" + }, + "allow_duplicate": { + "name": "Yinelenmesine İzin Ver", + "description": "Aynı medyanın birden çok kez indirilebilmesini sağlar" + }, + "merge_overlays": { + "name": "Kaplamaları Birleştirme", + "description": "Bir Snap'in Metnini ve ortamını tek bir dosyada birleştirir" + }, + "force_image_format": { + "name": "Görüntü Formatını Zorla", + "description": "Görüntülerin belirli bir Formatta kaydedilmesini zorlar" + }, + "force_voice_note_format": { + "name": "Ses Biçimini Zorla", + "description": "Sesli Notların belirli bir Formatta kaydedilmesini zorlar" + }, + "download_profile_pictures": { + "name": "Profil Resimlerini İndir", + "description": "Profil Resimlerini profil sayfasından indirmenize olanak sağlar" + }, + "chat_download_context_menu": { + "name": "Sohbet İndirme İçerik Menüsü", + "description": "Bir konuşmadaki medyayı uzun basarak indirmenize olanak sağlar" + }, + "ffmpeg_options": { + "name": "FFmpeg Ayarları", + "description": "Ek FFmpeg seçeneklerini belirleyin", + "properties": { + "threads": { + "name": "İş Parçacıkları", + "description": "Kullanılacak iş parçacığı miktarı" + }, + "preset": { + "name": "Ön Ayar", + "description": "Dönüştürme hızını ayarlayın" + }, + "constant_rate_factor": { + "name": "Sabit Hız Faktörü", + "description": "Video kodlayıcı için sabit hız faktörünü ayarlayın\nlibx264 için 0 ile 51 arasında" + }, + "video_bitrate": { + "name": "Video Bit Hızı", + "description": "Video bit hızını ayarlayın (kbps)" + }, + "audio_bitrate": { + "name": "Ses Bit Hızı", + "description": "Ses bit hızını ayarlayın (kbps)" + }, + "custom_video_codec": { + "name": "Özel Video Codec'i", + "description": "Özel bir Video Codec'i ayarlayın (örn. libx264)" + }, + "custom_audio_codec": { + "name": "Özel Ses Codec'i", + "description": "Özel bir Ses Codec'i ayarlayın (örn. AAC)" + } + } + }, + "logging": { + "name": "Günlük Kaydı", + "description": "Medya indirilirken tost mesajlarını gösterir" + }, + "custom_path_format": { + "description": "İndirilen medya için özel bir yol biçimi belirtin\n\nKullanılabilir değişkenler:\n - %username%\n - %source%\n - %hash%\n - %date_time%", + "name": "Özel Yol Formatı" + }, + "opera_download_button": { + "description": "Bir Snap görüntülerken sağ üst köşeye bir indirme düğmesi ekler", + "name": "Opera İndirme Düğmesi" + } + } + }, + "user_interface": { + "name": "Kullanıcı Arayüzü", + "description": "Snapchat'in görünümünü ve hissini değiştirin", + "properties": { + "enable_app_appearance": { + "name": "Uygulama Görünüm Ayarlarını Etkinleştir", + "description": "Gizli Uygulama Görünümü Ayarını etkinleştirir\nDaha yeni Snapchat sürümlerinde gerekli olmayabilir" + }, + "amoled_dark_mode": { + "name": "AMOLED Karanlık Mod", + "description": "AMOLED karanlık modunu etkinleştirir\nSnapchat'in karanlık modunun etkin olduğundan emin olun" + }, + "friend_feed_message_preview": { + "name": "Arkadaş Akışı Mesaj Önizlemesi", + "description": "Arkadaş Akışındaki son mesajların önizlemesini gösterir", + "properties": { + "amount": { + "name": "Miktar", + "description": "Önizlemesi yapılacak mesaj miktarı" + } + } + }, + "bootstrap_override": { + "name": "Önyükleme Geçersiz Kılma", + "description": "Kullanıcı arayüzü önyükleme ayarlarını geçersiz kılar", + "properties": { + "app_appearance": { + "name": "Uygulama Görünümü", + "description": "Kalıcı bir Uygulama Görünümü ayarlar" + }, + "home_tab": { + "name": "Ana Sayfa Sekmesi", + "description": "Snapchat açılırken başlangıç sekmesini geçersiz kılar" + } + } + }, + "map_friend_nametags": { + "name": "Geliştirilmiş Arkadaş Haritası İsim Etiketleri", + "description": "Snapmap'teki arkadaşların İsim Etiketlerini iyileştirir" + }, + "streak_expiration_info": { + "name": "Seri Sona Erme Bilgisini Göster", + "description": "Seriler sayacının yanında bir Seri Sona Erme zamanlayıcısı gösterir" + }, + "hide_friend_feed_entry": { + "name": "Arkadaş Akışı Girişini Gizle", + "description": "Arkadaş Akışı'ndan belirli bir arkadaşı gizler\nBu özelliği yönetmek için sosyal sekmesini kullanın" + }, + "hide_streak_restore": { + "name": "Seri Geri Yüklemeyi Gizle", + "description": "Arkadaş akışındaki Seri Geri Yükle düğmesini gizler" + }, + "hide_story_sections": { + "name": "Hikaye Bölümünü Gizle", + "description": "Hikaye bölümünde gösterilen belirli arayüz öğelerini gizle" + }, + "hide_ui_components": { + "name": "Kullanıcı Arayüzü Bileşenlerini Gizle", + "description": "Hangi kullanıcı arayüzü bileşenlerinin gizleneceğini seçin" + }, + "disable_spotlight": { + "name": "Spotlight'ı Devre Dışı Bırak", + "description": "Spotlight sayfasını devre dışı bırakır" + }, + "friend_feed_menu_buttons": { + "name": "Arkadaş Akışı Menü Düğmeleri", + "description": "Arkadaş Akışı Menü Çubuğunda hangi düğmelerin gösterileceğini seçin" + }, + "friend_feed_menu_position": { + "name": "Arkadaş Akışı Pozisyonu Dizini", + "description": "Arkadaş Akışı Menüsü bileşeninin konumu" + }, + "enable_friend_feed_menu_bar": { + "name": "Arkadaş Akışı Menü Çubuğu", + "description": "Yeni Arkadaş Akışı Menüsü Çubuğunu etkinleştirir" + }, + "fidelius_indicator": { + "name": "Fidelius Göstergesi", + "description": "Sadece size gönderilen mesajların yanına yeşil bir nokta ekler" + }, + "hide_settings_gear": { + "name": "Ayarlar Dişlisini Gizle", + "description": "Arkadaş akışındaki SnapEnhance Ayarları Dişlisini gizler" + }, + "opera_media_quick_info": { + "description": "Opera görüntüleyici içerik menüsünde oluşturma tarihi gibi medyanın yararlı bilgilerini gösterir", + "name": "Opera Medya Hızlı Bilgi" + }, + "vertical_story_viewer": { + "name": "Dikey Hikaye Görüntüleyici", + "description": "Tüm hikayeler için dikey hikaye görüntüleyiciyi etkinleştirir" + }, + "old_bitmoji_selfie": { + "name": "Eski Bitmoji Selfie'si", + "description": "Eski Snapchat sürümlerindeki Bitmoji Selfie'lerini geri getirir" + }, + "hide_quick_add_friend_feed": { + "description": "Arkadaş akışındaki Hızlı Ekleme bölümünü gizler", + "name": "Arkadaş Akışında Hızlı Eklemeyi Gizle" + }, + "prevent_message_list_auto_scroll": { + "name": "Mesaj Listesi Otomatik Kaydırmayı Önle", + "description": "Mesaj gönderirken/alırken mesaj listesinin en alta kaymasını engeller" + }, + "edit_text_override": { + "name": "Metin Geçersiz Kılmayı Düzenle", + "description": "Metin alanı davranışını geçersiz kılar" + }, + "snap_preview": { + "name": "Snap Önizleme", + "description": "Sohbette açılmayan Snap'lerin yanında küçük bir önizleme görüntüler" + } + } + }, + "messaging": { + "name": "Mesajlaşma", + "description": "Arkadaşlarınızla etkileşim şeklinizi değiştirin", + "properties": { + "anonymous_story_viewing": { + "name": "Anonim Hikaye Görüntüleme", + "description": "Herhangi birinin hikayelerini gördüğünüzü bilmesini engeller" + }, + "hide_bitmoji_presence": { + "name": "Bitmoji'yi Gizle", + "description": "Sohbet sırasında Bitmoji'nizin görünmesini engeller" + }, + "hide_typing_notifications": { + "name": "Yazıyor Bildirimlerini Gizle", + "description": "Herhangi birinin mesaj yazdığınızı bilmesini engeller" + }, + "unlimited_snap_view_time": { + "name": "Sınırsız Snap Görüntüleme Süresi", + "description": "Snap'leri görüntülemek için Zaman Sınırını kaldırır" + }, + "disable_replay_in_ff": { + "name": "AA'da Tekrar Oynatmayı Devre Dışı Bırak", + "description": "Arkadaş Akışından uzun basarak yeniden oynatma özelliğini devre dışı bırakır" + }, + "message_preview_length": { + "name": "Mesaj Önizleme Uzunluğu", + "description": "Önizlenecek mesaj miktarını belirtin" + }, + "prevent_message_sending": { + "name": "Mesaj Gönderimini Önleme", + "description": "Belirli mesaj türlerinin gönderilmesini engeller" + }, + "better_notifications": { + "name": "Daha İyi Bildirimler", + "description": "Alınan bildirimlere daha fazla bilgi ekler" + }, + "notification_blacklist": { + "name": "Bildirim Kara Listesi", + "description": "Engellenecek bildirimleri seçin" + }, + "message_logger": { + "name": "Mesaj Kaydedici", + "description": "Mesajların silinmesini önler", + "properties": { + "message_filter": { + "name": "Mesaj Filtresi", + "description": "Hangi mesajların günlüğe kaydedileceğini seçin (tüm mesajlar için boş bırakın)" + }, + "auto_purge": { + "description": "Belirtilen süreden daha eski olan önbelleğe alınmış mesajları otomatik olarak siler", + "name": "Otomatik Temizleme" + }, + "keep_my_own_messages": { + "name": "Kendi Mesajlarımı Sakla", + "description": "Kendi mesajlarınızın silinmesini önler" + } + } + }, + "auto_save_messages_in_conversations": { + "name": "Mesajları Otomatik Kaydet", + "description": "Görüşmelerdeki her mesajı otomatik olarak kaydeder" + }, + "gallery_media_send_override": { + "name": "Galeri Medya Göndermesini Geçersiz Kılma", + "description": "Galeri'den gönderirken medya kaynağını taklit eder" + }, + "call_start_confirmation": { + "name": "Arama Başlatma Onayı", + "description": "Arama başlatırken bir onay iletişim kutusu gösterir" + }, + "half_swipe_notifier": { + "properties": { + "min_duration": { + "name": "Minimum Süre", + "description": "Yarım kaydırmanın minimum süresi (saniye cinsinden)" + }, + "max_duration": { + "description": "Yarım kaydırmanın maksimum süresi (saniye cinsinden)", + "name": "Maksimum Süre" + } + }, + "name": "Yarım Kaydırma Bildiricisi", + "description": "Birisi konuşmaya yarım kaydırma yaptığında sizi bilgilendirir" + }, + "bypass_screenshot_detection": { + "description": "Snapchat'in ekran görüntüsü aldığınızı algılamasını engeller", + "name": "Ekran Görüntüsü Algılamayı Kapat" + }, + "strip_media_metadata": { + "description": "Mesaj olarak göndermeden önce medyanın meta verilerini kaldırır", + "name": "Medya Meta Verilerini Kaldır" + }, + "bypass_message_retention_policy": { + "name": "Mesaj Saklama Politikasını Atlayın", + "description": "Mesajların görüntülendikten sonra silinmesini önler" + }, + "prevent_story_rewatch_indicator": { + "name": "Hikaye Tekrar İzleme Göstergesini Önle", + "description": "Herhangi birinin hikayelerini tekrar izlediğinizi bilmesini engeller" + }, + "instant_delete": { + "name": "Anında Silme", + "description": "Mesajları silerken onay iletişim kutusunu kaldırır" + }, + "hide_peek_a_peek": { + "description": "Bir sohbete yarım kaydırma yaptığınızda bildirim gönderilmesini önler", + "name": "Peek-a-Peek'i Gizle" + }, + "loop_media_playback": { + "name": "Medya Oynatmayı Döngüye Al", + "description": "Snap'leri / Hikayeleri görüntülerken medya oynatmayı döngüye alır" + } + } + }, + "global": { + "name": "Genel", + "description": "Genel Snapchat Ayarlarını Değiştirin", + "properties": { + "spoofLocation": { + "name": "Konum", + "description": "Konumunuzu taklit edin", + "properties": { + "coordinates": { + "name": "Koordinatlar", + "description": "Koordinatları ayarlayın" + } + } + }, + "snapchat_plus": { + "name": "Snapchat Plus", + "description": "Snapchat Plus özelliklerini etkinleştirir\nBazı Sunucu taraflı özellikler çalışmayabilir" + }, + "auto_updater": { + "name": "Otomatik Güncelleyici", + "description": "Yeni güncellemeleri otomatik olarak kontrol eder" + }, + "disable_metrics": { + "name": "Ölçümleri Devre Dışı Bırak", + "description": "Snapchat'e belirli analitik verilerin gönderilmesini engeller" + }, + "block_ads": { + "name": "Reklamları Engelle", + "description": "Reklamların görüntülenmesini engeller" + }, + "bypass_video_length_restriction": { + "name": "Video Uzunluğu Kısıtlamalarını Atlayın", + "description": "Tek: tek bir video gönderir\nBöl: videoları düzenledikten sonra böl" + }, + "disable_google_play_dialogs": { + "name": "Google Play Hizmetleri İletişim Kutularını Devre Dışı Bırak", + "description": "Google Play Hizmetleri mevcutluk iletişim kutularının gönderilmesini önlemer" + }, + "disable_snap_splitting": { + "name": "Snap Bölmeyi Devre Dışı Bırak", + "description": "Snap'lerin birden fazla parçaya bölünmesini önler\nGönderdiğiniz resimler videoya dönüşecek" + }, + "disable_confirmation_dialogs": { + "name": "Onay İletişim Kutularını Devre Dışı Bırak", + "description": "Seçilen eylemleri otomatik olarak onaylar" + }, + "suspend_location_updates": { + "name": "Konum Güncellemelerini Askıya Al", + "description": "Harita ayarlarına konum güncellemelerini askıya almak için bir düğme ekler" + }, + "spotlight_comments_username": { + "name": "Spotlight Yorumlar Kullanıcı Adı", + "description": "Spotlight yorumlarında yazar kullanıcı adını gösterir" + }, + "disable_public_stories": { + "name": "Halka Açık Hikayeleri Devre Dışı Bırak", + "description": "Herkese açık tüm hikayeleri Keşfet sayfasından kaldırır\nDüzgün çalışması için önbelleğin temizlenmesi gerekebilir" + }, + "force_upload_source_quality": { + "name": "Kaynak Kalitesini Yüklemeye Zorla", + "description": "Snapchat'i medyayı orijinal kalitesinde yüklemeye zorlar\nLütfen bunun medyadan meta verileri kaldırmayabileceğini unutmayın" + } + } + }, + "rules": { + "name": "Kurallar", + "description": "Tek tek kişiler için Otomatik Özellikleri Yönetme" + }, + "camera": { + "name": "Kamera", + "description": "Mükemmel çekim için doğru ayarları yapın", + "properties": { + "disable_camera": { + "name": "Kamerayı Devre Dışı Bırak", + "description": "Snapchat'in cihazınızda bulunan kameraları kullanmasını engeller" + }, + "immersive_camera_preview": { + "name": "Sürükleyici Önizleme", + "description": "Snapchat'in Kamera önizlemesini Kırpmasını Önler\nBu, kameranın bazı cihazlarda titremesine neden olabilir" + }, + "override_preview_resolution": { + "name": "Önizleme Çözünürlüğünü Geçersiz Kıl", + "description": "Kamera Önizleme Çözünürlüğünü geçersiz kılar" + }, + "override_picture_resolution": { + "name": "Resim Çözünürlüğünü Geçersiz Kıl", + "description": "Resim çözünürlüğünü geçersiz kılar" + }, + "custom_frame_rate": { + "name": "Özel Kare Hızı", + "description": "Kamera kare hızını geçersiz kılar" + }, + "force_camera_source_encoding": { + "name": "Kamera Kaynağı Kodlamasını Zorla", + "description": "Kamera kaynak kodlamasını zorlar" + }, + "custom_preview_resolution": { + "description": "Özel bir kamera önizleme çözünürlüğü, genişlik x yükseklik (örn. 1920x1080) ayarlar.\nÖzel çözünürlük cihazınız tarafından desteklenmelidir", + "name": "Özel Önizleme Çözünürlüğü" + }, + "hevc_recording": { + "name": "HEVC Kaydı", + "description": "Video kaydı için HEVC (H.265) codec bileşenini kullanır" + }, + "custom_picture_resolution": { + "description": "Genişlik x yükseklik olmak üzere özel bir resim çözünürlüğü ayarlar (örn. 1920x1080).\nÖzel çözünürlük cihazınız tarafından desteklenmelidir", + "name": "Özel Resim Çözünürlüğü" + }, + "black_photos": { + "description": "Çekilen fotoğrafları siyah bir arka planla değiştirir\nVideolar etkilenmez", + "name": "Siyah Fotoğraflar" + } + } + }, + "streaks_reminder": { + "name": "Seri Hatırlatma", + "description": "Seri'leriniz hakkında sizi periyodik olarak bilgilendirir", + "properties": { + "interval": { + "name": "Aralık", + "description": "Her hatırlatma arasındaki aralık (saat)" + }, + "remaining_hours": { + "name": "Kalan Süre", + "description": "Bildirim gösterilmeden önce kalan süre" + }, + "group_notifications": { + "name": "Bildirimleri Grupla", + "description": "Bildirimleri tek bir bildirimde gruplama" + } + } + }, + "experimental": { + "name": "Deneysel", + "description": "Deneysel özellikler", + "properties": { + "native_hooks": { + "name": "Yerel Kancalar", + "description": "Snapchat'in yerel koduna bağlanan Güvenli Olmayan Özellikler", + "properties": { + "disable_bitmoji": { + "name": "Bitmoji'yi Devre Dışı Bırak", + "description": "Arkadaş Profili Bitmoji'sini devre dışı bırakır" + } + } + }, + "spoof": { + "name": "Taklit", + "description": "Hakkınızdaki çeşitli bilgileri taklit eder", + "properties": { + "randomize_persistent_device_token": { + "description": "Her oturum açma işleminden sonra rastgele bir cihaz belirteci oluşturur", + "name": "Kalıcı Cihaz Belirtecini Rastgele Ayarlama" + }, + "remove_mock_location_flag": { + "name": "Sahte Konum İşaretini Kaldır", + "description": "Snapchat'in Mock konumunu algılamasını engeller" + }, + "android_id": { + "name": "Android Kimliği", + "description": "Android kimliğinizi belirtilen değerle değiştirir" + }, + "fingerprint": { + "description": "Cihazınızın Parmak İzini Taklit Eder", + "name": "Cihaz Parmak İzi" + }, + "remove_vpn_transport_flag": { + "description": "Snapchat'in VPN'leri algılamasını engeller", + "name": "VPN Aktarım İşaretini Kaldır" + }, + "play_store_installer_package_name": { + "description": "Yükleyici paket adını com.android.vending olarak geçersiz kılar", + "name": "Play Store Yükleyici Paket Adı" + } + } + }, + "app_passcode": { + "name": "Uygulama Şifresi", + "description": "Uygulamayı kilitlemek için bir parola ayarlar" + }, + "app_lock_on_resume": { + "name": "Uygulama Yeniden Açıldığında Kilit", + "description": "Uygulama yeniden açıldığında kilitlenir" + }, + "infinite_story_boost": { + "name": "Sonsuz Hikaye Takviyesi", + "description": "Hikaye Takviye Limiti gecikmesini atlayın" + }, + "meo_passcode_bypass": { + "name": "My Eyes Only Şifresini Kır", + "description": "My Eyes Only şifresini atlayın\nBu yalnızca parola daha önce doğru girilmişse çalışacaktır" + }, + "unlimited_multi_snap": { + "name": "Sınırsız Çoklu Snap", + "description": "Sınırsız Miktarda Çoklu Snap çekmenizi sağlar" + }, + "no_friend_score_delay": { + "name": "Arkadaş Puanı Gecikmesi Yok", + "description": "Arkadaş Skoru görüntülenirken yaşanan gecikmeyi kaldırır" + }, + "e2ee": { + "name": "Uçtan-Uca Şifreleme", + "description": "Paylaşılan bir gizli anahtar kullanarak mesajlarınızı AES ile şifreler\nAnahtarınızı güvenli bir yere kaydettiğinizden emin olun!", + "properties": { + "encrypted_message_indicator": { + "name": "Şifrelenmiş Mesaj Göstergesi", + "description": "Şifrelenmiş mesajların yanına bir 🔒 emojisi ekler" + }, + "force_message_encryption": { + "name": "Mesaj Şifrelemeyi Zorla", + "description": "Yalnızca birden fazla konuşma seçildiğinde E2E Şifrelemesi etkin olmayan kişilere şifreli mesaj gönderilmesini engeller" + } + } + }, + "add_friend_source_spoof": { + "name": "Arkadaş Kaynağı Taklidi Ekle", + "description": "Arkadaşlık İsteğinin kaynağını taklit eder" + }, + "hidden_snapchat_plus_features": { + "name": "Gizli Snapchat Plus Özellikleri", + "description": "Yayınlanmamış/beta Snapchat Plus özelliklerini etkinleştirir\nEski Snapchat sürümlerinde çalışmayabilir" + }, + "disable_composer_modules": { + "name": "Composer Modüllerini Devre Dışı Bırakma", + "description": "Seçili composer modüllerinin yüklenmesini engeller\nİsimler virgül ile ayrılmalıdır" + }, + "prevent_forced_logout": { + "name": "Zorla Oturum Kapatmayı Önleme", + "description": "Başka bir cihazdan giriş yaptığınızda Snapchat'in oturumunuzu kapatmasını engeller" + }, + "convert_message_locally": { + "description": "Snap'leri yerel olarak sohbet harici ortamına dönüştürür. Bu, sohbet indirme içerik menüsünde görünür", + "name": "Mesajı Yerel Olarak Dönüştür" + }, + "story_logger": { + "description": "Arkadaş hikayelerinin bir tarihçesini sunar", + "name": "Hikaye Kaydedici" + } + } + }, + "scripting": { + "name": "Komut Dosyaları", + "description": "SnapEnhance'i genişletmek için özel komut dosyaları çalıştırın", + "properties": { + "developer_mode": { + "name": "Geliştirici Modu", + "description": "Snapchat'in kullanıcı arayüzünde hata ayıklama bilgilerini gösterir" + }, + "module_folder": { + "name": "Modül Klasörü", + "description": "Komut dosyalarının bulunduğu klasör" + }, + "integrated_ui": { + "name": "Entegre Kullanıcı Arayüzü", + "description": "Komut dosyalarının Snapchat'e özel kullanıcı arayüzü bileşenleri eklemesine izin verir" + }, + "disable_log_anonymization": { + "description": "Günlüklerin anonimleştirilmesini devre dışı bırakır", + "name": "Günlük Anonimleştirmeyi Devre Dışı Bırak" + }, + "auto_reload": { + "description": "Değiştiklerinde komut dosyalarını otomatik olarak yeniden yükler", + "name": "Otomatik Yeniden Yükleme" + } + } + } + }, + "options": { + "app_appearance": { + "always_light": "Daima Aydınlık", + "always_dark": "Daima Koyu" + }, "better_notifications": { - "chat": "Sohbet mesajlarını göster", - "snap": "Medyaları göster", - "reply_button": "Yanıtlama düğmesi ekle", - "download_button": "İndirme düğmesi ekle" + "reply_button": "Yanıtla düğmesi ekle", + "download_button": "İndirme düğmesi ekle", + "group": "Bildirimleri gruplandır", + "mark_as_read_and_save_in_chat": "Okundu olarak işaretlendiğinde Sohbet'e kaydet (Otomatik Kaydet'e bağlıdır)", + "mark_as_read_button": "Okundu olarak işaretle düğmesi", + "media_preview": "Medyanın önizlemesini göster", + "chat_preview": "Sohbetin önizlemesini göster" }, "friend_feed_menu_buttons": { - "auto_download_blacklist": "⬇️ Kara Liste Otomatik İndirme", - "anti_auto_save": "💬 Anti Otomatik Mesaj Kaydetme", - "stealth_mode": "👻 Gizli Mod", - "conversation_info": "👤 Konuşma Bilgileri" - }, - "download_options": { - "allow_duplicate": "Çift indirmelere izin ver", - "create_user_folder": "Her kullanıcı için klasör oluştur", + "auto_download": "⬇️ Otomatik İndirme", + "auto_save": "💬 Mesajları Otomatik Kaydetme", + "stealth": "👻 Gizli Mod", + "conversation_info": "👤 Konuşma Bilgileri", + "e2e_encryption": "🔒 E2E Şifreleme Kullan", + "mark_stories_as_seen_locally": "👀 Hikayeleri görüldü olarak işaretleyin", + "mark_snaps_as_seen": "👀 Snap'leri görüldü olarak işaretleyin", + "unsaveable_messages": "⬇️ Kaydedilemeyen Mesajlar" + }, + "path_format": { + "create_author_folder": "Her kullanıcı için klasör oluştur", + "create_source_folder": "Her medya kaynağı türü için klasör oluşturma", "append_hash": "Dosya adına benzersiz bir hash ekle", + "append_source": "Medya kaynağını dosya adına ekleyin", "append_username": "Dosya adına kullanıcı adını ekle", - "append_date_time": "Dosya adına tarih ve saati ekle", - "append_type": "Medya türünü dosya adına ekle", - "merge_overlay": "Snap Görüntü Kaplamalarını Birleştirme" + "append_date_time": "Dosya adına tarih ve saati ekle" }, - "auto_download_options": { + "auto_download_sources": { "friend_snaps": "Arkadaş Snapleri", "friend_stories": "Arkadaş Hikayeleri", "public_stories": "Herkese Açık Hikayeler", "spotlight": "Spotlight" }, - "download_logging": { + "logging": { "started": "Başladı", "success": "Başarılı", - "progress": "Süreç", - "failure": "Hata" - }, - "auto_save_messages": { - "NOTE": "Sesli Not", - "CHAT": "Sohbet", - "EXTERNAL_MEDIA": "Harici Medya", - "SNAP": "Snap", - "STICKER": "Çıkartma" + "progress": "İlerleme", + "failure": "Başarısız" }, "notifications": { "chat_screenshot": "Ekran Görüntüsü", "chat_screen_record": "Ekran Kaydı", + "snap_replay": "Snap Tekrar Oynatma", "camera_roll_save": "Film Rulosu Kaydı", "chat": "Sohbet", "chat_reply": "Sohbet Cevabı", "snap": "Snap", "typing": "Yazıyor", "stories": "Hikayeler", + "chat_reaction": "DM Tepkisi", + "group_chat_reaction": "Grup Tepkisi", "initiate_audio": "Gelen Sesli Arama", "abandon_audio": "Cevapsız Sesli Arama", "initiate_video": "Gelen Görüntülü Arama", @@ -289,41 +788,82 @@ "ORIGINAL": "Orijinal", "NOTE": "Sesli Not", "SNAP": "Snap", - "LIVE_SNAP": "Sesli Snap" - }, - "hide_ui_elements": { - "remove_call_buttons": "Arama Butonlarını Kaldır", - "remove_cognac_button": "Konyak Butonunu Kaldır", - "remove_live_location_share_button": "Canlı Konum Paylaş Butonunu Kaldır", - "remove_stickers_button": "Çıkartmalar Butonunu Kaldır", - "remove_voice_record_button": "Ses Kayıt Butonunu Kaldır", - "remove_camera_borders": "Kamera Kenarlıklarını Kaldır" - }, - "auto_updater": { - "DISABLED": "Devre dışı", - "EVERY_LAUNCH": "Her Açılışta", - "DAILY": "Günlük", - "WEEKLY": "Haftalık" - }, - "story_viewer_override": { - "OFF": "Kapalı", - "DISCOVER_PLAYBACK_SEEKBAR": "Keşfet Oynatma Seekbarını Etkinleştir", - "VERTICAL_STORY_VIEWER": "Dikey Hikaye Görüntüleyiciyi Etkinleştir" - }, - "hide_story_section": { + "SAVABLE_SNAP": "Kaydedilebilir Snap" + }, + "hide_ui_components": { + "hide_profile_call_buttons": "Profil Arama Düğmelerini Kaldır", + "hide_chat_call_buttons": "Sohbet Arama Düğmelerini Kaldır", + "hide_live_location_share_button": "Canlı Konum Paylaş Düğmesini Kaldır", + "hide_stickers_button": "Çıkartmalar Butonunu Kaldır", + "hide_voice_record_button": "Ses Kayıt Butonunu Kaldır", + "hide_unread_chat_hint": "Okunmamış Sohbet İpucunu Kaldır" + }, + "hide_story_sections": { "hide_friend_suggestions": "Arkadaş önerilerini gizle", "hide_friends": "Arkadaşlar bölümünü gizle", - "hide_following": "Takip edilenler bölümünü gizle", - "hide_for_you": "Sizin İçin Bölümünü Gizle" - }, - "startup_page_override": { - "OFF": "Kapalı", - "ngs_map_icon_container": "Harita", - "ngs_chat_icon_container": "Sohbet", - "ngs_camera_icon_container": "Kamera", - "ngs_community_icon_container": "Topluluk / Hikayeler", - "ngs_spotlight_icon_container": "Spotlight", - "ngs_search_icon_container": "Ara" + "hide_suggested": "Önerilen bölümünü gizle", + "hide_for_you": "Sizin İçin Bölümünü Gizle", + "hide_suggested_friend_stories": "Önerilen arkadaş hikayelerini gizle" + }, + "home_tab": { + "map": "Harita", + "chat": "Sohbet", + "camera": "Kamera", + "discover": "Keşfet", + "spotlight": "Spotlight" + }, + "add_friend_source_spoof": { + "added_by_username": "Kullanıcı Adına Göre", + "added_by_mention": "Bahsetmeye Göre", + "added_by_group_chat": "Grup Sohbetine Göre", + "added_by_qr_code": "QR Kodu'na Göre", + "added_by_community": "Topluluğa Göre", + "added_by_quick_add": "Hızlı Ekle tarafından" + }, + "bypass_video_length_restriction": { + "single": "Tek medya", + "split": "Bölünmüş medya" + }, + "auto_reload": { + "snapchat_only": "Sadece Snapchat", + "all": "Tümü (Snapchat + SnapEnhance)" + }, + "strip_media_metadata": { + "remove_audio_note_duration": "Ses Notası Süresini Kaldır", + "remove_audio_note_transcript_capability": "Ses Notu Transkript Özelliğini Kaldır", + "hide_extras": "Ekstraları Gizle (örn. bahsedenler)", + "hide_caption_text": "Başlık Metnini Gizle", + "hide_snap_filters": "Snap Filtrelerini Gizle" + }, + "auto_purge": { + "1_day": "1 Gün", + "1_week": "1 Hafta", + "1_month": "1 Ay", + "2_weeks": "2 Hafta", + "never": "Asla", + "1_hour": "1 Saat", + "3_hours": "3 Saat", + "6_months": "6 Ay", + "3_days": "3 Gün", + "6_hours": "6 Saat", + "3_months": "3 Ay", + "12_hours": "12 Saat" + }, + "disable_confirmation_dialogs": { + "hide_conversation": "Konuşmayı Gizle", + "clear_conversation": "Arkadaş Akışından Konuşmayı Temizle", + "remove_friend": "Arkadaşı Kaldır", + "hide_friend": "Arkadaşı Gizle", + "ignore_friend": "Arkadaşı Yoksay", + "block_friend": "Arkadaşı Engelle" + }, + "edit_text_override": { + "bypass_text_input_limit": "Metin Giriş Sınırını Atla", + "multi_line_chat_input": "Çok Hatlı Sohbet Girişi" + }, + "old_bitmoji_selfie": { + "2d": "2D Bitmoji", + "3d": "3D Bitmoji" } } }, @@ -331,19 +871,24 @@ "preview": "Önizleme", "stealth_mode": "Gizli Mod", "auto_download_blacklist": "Kara Liste Otomatik İndirme", - "anti_auto_save": "Anti Otomatik Kaydetme" - }, - "message_context_menu_option": { - "download": "İndir", - "preview": "Önizleme" + "anti_auto_save": "Anti Otomatik Kaydetme", + "mark_snaps_as_seen": "Snap'leri Görüldü Olarak İşaretle", + "mark_stories_as_seen_locally": "Hikayeleri yerel olarak görüldü olarak işaretle" }, "chat_action_menu": { "preview_button": "Önizleme", "download_button": "İndir", - "delete_logged_message_button": "Kaydedilen Mesajı Sil" + "delete_logged_message_button": "Kaydedilen Mesajı Sil", + "convert_message": "Mesajı Dönüştür" }, "opera_context_menu": { - "download": "Medyayı İndir" + "download": "Medyayı İndir", + "media_duration": "Medya süresi: {duration} ms", + "show_debug_info": "Hata Ayıklama Bilgilerini Göster", + "expires_at": "{date}'de sona erer", + "created_at": "{date}'te oluşturuldu", + "sent_at": "{date}'de gönderildi", + "media_size": "Medya boyutu: {size}" }, "modal_option": { "profile_info": "Profil Bilgisi", @@ -360,26 +905,22 @@ }, "profile_info": { "title": "Profil Bilgisi", - "username": "Kullanıcı Adı", + "first_created_username": "İlk Oluşturulan Kullanıcı Adı", + "mutable_username": "Değiştirilebilir Kullanıcı Adı", "display_name": "Görünen İsim", "added_date": "Eklenme Tarihi", - "birthday": "Doğum Günü: {day} {month}" - }, - "auto_updater": { - "no_update_available": "Güncelleme yok!", - "dialog_title": "Güncelleme mevcut!", - "dialog_message": "SnapEnhance için yeni bir güncelleme mevcut! ({version})\n\n{body}", - "dialog_positive_button": "İndir ve Yükle", - "dialog_negative_button": "İptal", - "downloading_toast": "Güncelleme İndiriliyor...", - "download_manager_notification_title": "SnapEnhance APK'sı indiriliyor..." + "birthday": "Doğum Günü: {day} {month}", + "friendship": "Arkadaşlık", + "add_source": "Kaynak Ekle", + "snapchat_plus": "Snapchat Plus", + "snapchat_plus_state": { + "subscribed": "Abone Olunanlar", + "not_subscribed": "Abone Olunmayanlar" + }, + "hidden_birthday": "Doğum Günü : Gizli" }, "chat_export": { - "select_export_format": "Dışa Aktarma Formatını Seçin", - "select_media_type": "Dışa Aktarılacak Medya Türlerini Seçin", - "select_conversation": "Dışa aktarmak için bir Konuşma seçin", "dialog_negative_button": "İptal", - "dialog_neutral_button": "Tümünü Dışa Aktar", "dialog_positive_button": "Dışa Aktar", "exported_to": "{path}'a aktarıldı", "exporting_chats": "Sohbetler Dışa Aktarılıyor...", @@ -388,44 +929,44 @@ "writing_output": "Çıktı yazılıyor...", "finished": "Bitti! Artık bu iletişim kutusunu kapatabilirsiniz.", "no_messages_found": "Mesaj bulunamadı!", - "exporting_message": "{conversation} dışa aktarılıyor..." + "exporting_message": "{conversation} dışa aktarılıyor...", + "exporter_dialog": { + "text_field_selection_all": "Tümü", + "export_file_format_title": "Dosya Formatını Dışa Aktar", + "download_medias_title": "Medyaları İndirin", + "amount_of_messages_title": "Mesaj Miktarı (hepsi için boş bırakın)", + "message_type_filter_title": "Mesajları Türe Göre Filtreleme", + "text_field_selection": "{amount} seçildi", + "select_conversations_title": "Konuşmaları Seç" + } }, "button": { "ok": "Tamam", "positive": "Evet", "negative": "Hayır", "cancel": "İptal", - "open": "Aç" + "open": "Aç", + "download": "İndir" }, - "download_manager_activity": { - "remove_all_title": "Tüm İndirmeleri Kaldır", - "remove_all_text": "Bunu yapmak istediğine emin misin?", - "remove_all": "Tümünü Kaldır", - "no_downloads": "İndirme yok", - "cancel": "İptal", - "file_not_found_toast": "Dosya yok!", - "category": { - "all_category": "Tümü", - "pending_category": "Beklemede", - "snap_category": "Snapler", - "story_category": "Hikayeler", - "spotlight_category": "Spotlight" - }, - "debug_settings": "Hata Ayıklama Ayarları", - "debug_settings_page": { - "clear_file_title": "{file_name} dosyasını temizle", - "clear_file_confirmation": "{file_name} dosyasını temizlemek istediğinizden emin misiniz?", - "clear_cache_title": "Önbelleği Temizle", - "reset_all_title": "Tüm ayarları sıfırla", - "reset_all_confirmation": "Tüm ayarları sıfırlamak istediğinizden emin misiniz?", - "success_toast": "Başarılı!", - "device_spoofer": "Cihaz Spoofer" - } + "profile_picture_downloader": { + "button": "Profil Resmini İndir", + "title": "Profil Resmi İndirici", + "avatar_option": "Profil Resmi", + "background_option": "Arkaplan" }, "download_processor": { + "attachment_type": { + "snap": "Snap", + "sticker": "Çıkartma", + "external_media": "Harici Medya", + "note": "Not", + "original_story": "Orijinal Hikaye" + }, + "select_attachments_title": "İndirilecek ekleri seçin", "download_started_toast": "İndirme başladı", "unsupported_content_type_toast": "Desteklenmeyen içerik türü!", "failed_no_longer_available_toast": "Medya artık mevcut değil", + "no_attachments_toast": "Ek bulunamadı!", "already_queued_toast": "Medya zaten sırada!", "already_downloaded_toast": "Medya zaten indirildi!", "saved_toast": "{path}'a kaydedildi", @@ -433,15 +974,102 @@ "processing_toast": "{path} işleniyor...", "failed_generic_toast": "İndirme başarısız oldu", "failed_to_create_preview_toast": "Önizleme oluşturulamadı", - "failed_processing_toast": "{error} işlenemedi", + "failed_processing_toast": "İşlem başarısız {error}", "failed_gallery_toast": "Galeriye kaydedilemedi {error}" }, - "config_activity": { - "title": "SnapEnhance Ayarları", - "selected_text": "{count} seçildi", - "invalid_number_toast": "Geçersiz sayı!" + "streaks_reminder": { + "notification_title": "Seriler", + "notification_text": "{friend} ile olan Serini {hoursLeft} saat içinde kaybedeceksin" + }, + "content_type": { + "FAMILY_CENTER_INVITE": "Aile Merkezi Davetiyesi", + "STATUS_CONVERSATION_CAPTURE_RECORD": "Ekran Kaydı", + "STATUS_CALL_MISSED_VIDEO": "Cevapsız Görüntülü Arama", + "CREATIVE_TOOL_ITEM": "Yaratıcı Araç Öğesi", + "STICKER": "Çıkartma", + "TINY_SNAP": "Minik Snap", + "STATUS_SAVE_TO_CAMERA_ROLL": "Film Rulosuna Kaydedildi", + "EXTERNAL_MEDIA": "Harici Medya", + "SNAP": "Snap", + "LOCATION": "Konum", + "CHAT": "Sohbet", + "STATUS_PLUS_GIFT": "Durum Artı Hediyesi", + "STATUS_COUNTDOWN": "Geri Sayım", + "LIVE_LOCATION_SHARE": "Canlı Konum Paylaşımı", + "STATUS": "Durum", + "STATUS_CONVERSATION_CAPTURE_SCREENSHOT": "Ekran görüntüsü", + "FAMILY_CENTER_ACCEPT": "Aile Merkezi Kabul", + "FAMILY_CENTER_LEAVE": "Aile Merkezi Ayrıl", + "STATUS_CALL_MISSED_AUDIO": "Cevapsız Sesli Arama", + "NOTE": "Sesli Not" + }, + "suspend_location_updates": { + "switch_text": "Konum Güncellemelerini Askıya Al" + }, + "better_notifications": { + "button": { + "download": "İndir", + "reply": "Yanıtla", + "mark_as_read": "Okundu olarak işaretle" + }, + "stealth_mode_notice": "Gizli modda okundu olarak işaretlenemiyor" + }, + "half_swipe_notifier": { + "notification_content_group": "{friend} {duration} saniye boyunca {group}'a yarım kaydırma yaptı", + "notification_channel_name": "Yarım Kaydırma", + "notification_content_dm": "{friend} sohbetinize {duration} saniye boyunca yarım kaydırma yaptı" + }, + "friendship_link_type": { + "mutual": "Karşılıklı", + "deleted": "Silinen", + "following": "Takip Edilen", + "incoming_follower": "Gelen Takipçi", + "incoming": "Gelen", + "blocked": "Engellenen", + "suggested": "Önerilen", + "outgoing": "Giden" + }, + "call_start_confirmation": { + "dialog_message": "Bir arama başlatmak istediğinizden emin misiniz?", + "dialog_title": "Arama Başlat" + }, + "bulk_messaging_action": { + "choose_action_title": "Bir eylem seçin", + "progress_status": "{total} öğesinin {index} öğesi işleniyor", + "actions": { + "clear_conversations": "Konuşmaları Temizle", + "remove_friends": "Arkadaşları Kaldır" + }, + "selection_dialog_continue_button": "Devam et", + "confirmation_dialog": { + "message": "Bu, seçilen tüm arkadaşları etkileyecektir. Bu eylem geri alınamaz.", + "title": "Emin misiniz?" + } + }, + "media_download_source": { + "public_story": "Herkese Açık Hikaye", + "spotlight": "Spotlight", + "pending": "Beklemede", + "merged": "Birleştirilmiş", + "story_logger": "Hikaye Kaydedici", + "none": "Hiçbiri", + "profile_picture": "Profil Fotoğrafı", + "story": "Hikaye", + "chat_media": "Sohbet Medyası" }, - "spoof_activity": { - "title": "Spoof Ayarları" + "material3_strings": { + "date_input_invalid_not_allowed": "Geçersiz tarih", + "date_range_input_invalid_range_input": "Geçersiz tarih aralığı", + "date_range_picker_scroll_to_previous_month": "Önceki ay", + "date_picker_switch_to_input_mode": "Giriş", + "date_range_picker_day_in_range": "Seçilen", + "date_input_invalid_for_pattern": "Geçersiz tarih", + "date_picker_today_description": "Bugün", + "date_picker_switch_to_calendar_mode": "Takvim", + "date_range_picker_start_headline": "Şuradan", + "date_range_picker_end_headline": "Şuraya", + "date_range_picker_scroll_to_next_month": "Sonraki ay", + "date_range_picker_title": "Tarih aralığı seçin", + "date_input_invalid_year_range": "Geçersiz yıl" } -} \ No newline at end of file +} diff --git a/common/src/main/assets/lang/ur_IN.json b/common/src/main/assets/lang/ur_IN.json new file mode 100644 index 000000000..8667858db --- /dev/null +++ b/common/src/main/assets/lang/ur_IN.json @@ -0,0 +1,702 @@ +{ + "setup": { + "dialogs": { + "select_language": "زبان منتخب کریں", + "save_folder": "SnapEnhance کو سنیپ چیٹ سے میڈیا ڡن لینے اور بچانے کی اجازت دینے کے لئے اسٹوریج کی اجازت کی ضرورت ہوتی ہے۔\nبراہ کرم میڈیا ڈاؤن لوڈ کرنے کے لئے جگہ منتخب کریں۔", + "select_save_folder_button": "فولڈر منتخب کریں" + }, + "mappings": { + "dialog": "SnapEnhance کے صحیح طریقے سے کام کرنے کے لئے سنیپ چیٹ کے وسائط کی وسعت کو دعم دینے کے لئے ڈائنامک طریقے کی ضرورت ہوتی ہے، یہ زیادہ سے زیادہ 5 سیکنڈ نہیں لیگے گا۔", + "generate_button": "پیدا کریں", + "generate_failure_no_snapchat": "SnapEnhance نے سنیپ چیٹ کو پہچاننے میں ناکامی کا سامنا کیا، براہ کرم سنیپ چیٹ کو دوبارہ انسٹال کریں۔", + "generate_failure": "میپنگز تخلیق کرنے کی کوشش کرتے وقت ایک خرابی آئی، براہ کرم دوبارہ کوشش کریں۔", + "generate_success": "میپنگز کامیابی سے تخلیق کی گئی ہیں۔" + }, + "permissions": { + "dialog": "آپ جاری رہنے کے لئے مندرجہ ذیل ضروریات کو پورا کرنے کی ضرورت ہوتی ہے:", + "notification_access": "اطلاع کی رسائی", + "battery_optimization": "بیٹری کی بہترین استفادہ", + "display_over_other_apps": "دوسری ایپلیکیشنوں پر نمائش دینا", + "request_button": "درخواست دیں" + } + }, + "manager": { + "routes": { + "features": "خصوصیات", + "home": "ہوم", + "home_settings": "ترتیبات", + "home_logs": "لاگز", + "social": "سوشل", + "scripts": "اسکرپٹس" + }, + "sections": { + "home": { + "logs": { + "clear_logs_button": "لاگز صاف کریں", + "export_logs_button": "لاگز ایکسپورٹ کریں" + } + }, + "features": { + "disabled": "غیر فعال" + }, + "social": { + "e2ee_title": "آخر تک آخر اینڈ انکرپشن", + "rules_title": "قوانین", + "participants_text": "{count} مشترکین", + "not_found": "نہیں ملی", + "streaks_title": "استریکس", + "streaks_length_text": "لمبائی: {length}", + "streaks_expiration_short": "{hours}گنٹے", + "streaks_expiration_text": "میں ختم ہو جائے گا {eta}", + "reminder_button": "یاد دلانے کی ترتیب دیں" + } + }, + "dialogs": { + "add_friend": { + "title": "دوست یا گروپ شامل کریں", + "search_hint": "تلاش کریں", + "fetch_error": "ڈیٹا حاصل کرنے میں ناکامی", + "category_groups": "گروپس", + "category_friends": "دوست" + } + } + }, + "rules": { + "modes": { + "blacklist": "بلیک لسٹ موڈ", + "whitelist": "وائٹ لسٹ موڈ" + }, + "properties": { + "auto_download": { + "name": "آٹو ڈاؤن لوڈ", + "description": "سنیپس کو دیکھتے وقت خود بخود ڈاؤن لوڈ کریں", + "options": { + "blacklist": "آٹو ڈاؤن لوڈ سے خارج کریں", + "whitelist": "آٹو ڈاؤن لوڈ کریں" + } + }, + "stealth": { + "name": "چپ کرنے والا موڈ", + "description": "کسی کو بھی نے آپ کی سنیپس/چیٹس اور گفتگو کھولی ہو یا نہیں جاننے سے بچائیں", + "options": { + "blacklist": "چپ کرنے والا موڈ سے خارج کریں", + "whitelist": "چپ کرنے والا موڈ" + } + }, + "auto_save": { + "name": "آٹو بچانا", + "description": "میڈیا کی چیک کرتے وقت چیٹ میسیجز کو خود بخود بچائیں", + "options": { + "blacklist": "آٹو بچانا سے خارج کریں", + "whitelist": "آٹو بچانا" + } + }, + "hide_friend_feed": { + "name": "دوستوں کی فیڈ سے چھپائیں" + }, + "e2e_encryption": { + "name": "آخر تک آخر اینڈ انکرپشن استعمال کریں" + }, + "pin_conversation": { + "name": "بات چیت کو پن کریں" + } + } + }, + "actions": { + "clean_snapchat_cache": "سنیپ‌چیٹ کیش صاف کریں", + "clear_message_logger": "پیغام لاگر صاف کریں", + "refresh_mappings": "تصاویر کو تازہ کریں", + "open_map": "نقشہ پر مقام منتخب کریں", + "check_for_updates": "اپ ڈیٹس کی جانچ کاری کریں", + "export_chat_messages": "چیٹ میسیجز ایکسپورٹ کریں" + }, + "features": { + "notices": { + "unstable": "⚠ غیر مستحکم", + "ban_risk": "⚠ یہ خصوصیت پابن کا خدشہ ہوتا ہے", + "internal_behavior": "⚠ یہ سنیپ چیٹ کی اندرونی عمل کو توڑ سکتا ہے", + "require_native_hooks": "⚠ اس خصوصیت کے صحیح کام کے لئے تجرباتی نیٹوک ہکس کی ضرورت ہے" + }, + "properties": { + "downloader": { + "name": "ڈاؤن لوڈر", + "description": "اسنیپ چیٹ میڈیا ڈاؤن لوڈ کریں", + "properties": { + "save_folder": { + "name": "محفوظ کریں فولڈر", + "description": "وہ ڈائریکٹری منتخب کریں جہاں تمام میڈیا ڈاؤن لوڈ ہونا چاہئے" + }, + "auto_download_sources": { + "name": "آٹو ڈاؤن لوڈ ماخذات", + "description": "آٹومیٹک طریقے سے ڈاؤن لوڈ کرنے کے لئے ماخذات منتخب کریں" + }, + "prevent_self_auto_download": { + "name": "خود سے آٹو ڈاؤن لوڈ کو روکیں", + "description": "خود کے اپنے سنیپس کو خود بخود ڈاؤن لوڈ ہونے سے روکیں" + }, + "path_format": { + "name": "راستے کی شکل میں مخصوص کریں", + "description": "فائل راستے کی شکل مخصوص کریں" + }, + "allow_duplicate": { + "name": "میڈیا کی دوبارہ اجازت دیں", + "description": "ایک ہی میڈیا کو متعدد بار ڈاؤن لوڈ کرنے کی اجازت دیں" + }, + "merge_overlays": { + "name": "آورلے کو مرج کریں", + "description": "ایک سنیپ کے مواد اور مواد کو ایک فائل میں ملائیں" + }, + "force_image_format": { + "name": "تصویر کی شکل میں مجبور کریں", + "description": "تصویر کو مخصوص شکل میں محفوظ کرنے کی اجازت دیں" + }, + "force_voice_note_format": { + "name": "آوازی نوٹ کی شکل میں مجبور کریں", + "description": "آوازی نوٹس کو مخصوص شکل میں محفوظ کرنے کی اجازت دیں" + }, + "download_profile_pictures": { + "name": "پروفائل تصویریں ڈاؤن لوڈ کریں", + "description": "پروفائل صفحے سے پروفائل تصویریں ڈاؤن لوڈ کرنے کی اجازت دیں" + }, + "chat_download_context_menu": { + "name": "چیٹ ڈاؤن لوڈ مواقع مینو", + "description": "لانگ پریس کرکے بات چیت سے میڈیا ڈاؤن لوڈ کرنے کی اجازت دیں" + }, + "ffmpeg_options": { + "name": "ایف ایم پیگ اختیارات", + "description": "اضافی ایف ایم پیگ اختیارات مخصوص کریں", + "properties": { + "threads": { + "name": "تھریڈز", + "description": "استعمال کرنے والے تھریڈز کی مقدار" + }, + "preset": { + "name": "پریسٹ", + "description": "تبدیلی کی رفتار مخصوص کریں" + }, + "constant_rate_factor": { + "name": "مستقل شرح کا عامل", + "description": "ویڈیو اینکوڈر کے لئے مستقل شرح کا عامل مخصوص کریں\nلبیکس 264 کے لئے 0 سے 51 تک" + }, + "video_bitrate": { + "name": "ویڈیو بٹ ریٹ", + "description": "ویڈیو بٹ ریٹ (کلو بٹ فی سیکنڈ) مخصوص کریں" + }, + "audio_bitrate": { + "name": "آڈیو بٹ ریٹ", + "description": "آڈیو بٹ ریٹ (کلو بٹ فی سیکنڈ) مخصوص کریں" + }, + "custom_video_codec": { + "name": "کسٹم ویڈیو کوڈیک", + "description": "کسٹم ویڈیو کوڈیک مخصوص کریں (مثلاً لبیکس 264)" + }, + "custom_audio_codec": { + "name": "کسٹم آڈیو کوڈیک", + "description": "کسٹم آڈیو کوڈیک مخصوص کریں (مثلاً اے سی)" + } + } + }, + "logging": { + "name": "لاگنگ", + "description": "جب میڈیا ڈاؤن لوڈ ہوتا ہے تو توسٹ دکھائیں" + } + } + }, + "user_interface": { + "name": "صارف کا انٹرفیس", + "description": "سنیپ چیٹ کی لک اور محسوس تبدیل کریں", + "properties": { + "enable_app_appearance": { + "name": "ایپ کی ظاہریت کی سیٹنگز کو فعال کریں", + "description": "چھپی ہوئی ایپ کی ظاہریت کی سیٹنگ کو فعال کریں\nنئی ترین سنیپ چیٹ ورژنز پر شاید ضروری نہ ہو" + }, + "amoled_dark_mode": { + "name": "AMOLED ڈارک موڈ", + "description": "AMOLED ڈارک موڈ کو فعال کریں\nیقینی بنائیں کہ سنیپ چیٹ کا ڈارک موڈ فعال ہو" + }, + "friend_feed_message_preview": { + "name": "دوست فیڈ پیغام پیش نظر کریں", + "description": "دوست فیڈ میں پچھلے پیغاموں کی ایک مثال دیکھائیں", + "properties": { + "amount": { + "name": "مقدار", + "description": "پیش نظر کرنے والے پیغاموں کی مقدار" + } + } + }, + "bootstrap_override": { + "name": "بوٹسٹریپ کی بدلنے کی سیٹنگ", + "description": "صارف کا انٹرفیس بوٹسٹریپ کی سیٹنگ کو بدल دیتا ہے", + "properties": { + "app_appearance": { + "name": "ایپ کی ظاہریت", + "description": "مستقل ایپ کی ظاہریت تعین کرتا ہے" + }, + "home_tab": { + "name": "ہوم ٹیب", + "description": "سنیپ چیٹ کھولنے پر ابتدائی ٹیب کو بدلتا ہے" + } + } + }, + "map_friend_nametags": { + "name": "دوستوں کی سنیپ میپ نیم ٹیگز کو بہتر بنائیں", + "description": "سنیپ میپ پر دوستوں کی نیم ٹیگز کو بہتر بناتا ہے" + }, + "streak_expiration_info": { + "name": "استیک ختم ہونے کی معلومات دکھائیں", + "description": "استیک کاؤنٹر کے پاس ایک استیک ختم ہونے کا ٹائمر دکھاتا ہے" + }, + "hide_friend_feed_entry": { + "name": "دوست فیڈ انٹری چھپائیں", + "description": "دوست فیڈ میں ایک مخصوص دوست کو چھپائیں\nاس فیچر کو منظربند کرنے کے لئے سوشل ٹیب کا استعمال کریں" + }, + "hide_streak_restore": { + "name": "استیک کو بحال کرنے کا بٹن چھپائیں", + "description": "دوست فیڈ میں بحال کرنے کا بٹن چھپائیں" + }, + "hide_story_sections": { + "name": "کہانی کے حصے چھپائیں", + "description": "کہانی کے حصوں میں دکھائی جانے والی کچھ یو آئی عناصر چھپائیں" + }, + "hide_ui_components": { + "name": "یو آئی کے حصے چھپائیں", + "description": "چھپانے کے لئے یو آئی کمپوننٹس منتخب کریں" + }, + "disable_spotlight": { + "name": "اسپاٹ لائٹ کو غیر فعال کریں", + "description": "اسپاٹ لائٹ پیج کو غیر فعال کرتا ہے" + }, + "friend_feed_menu_buttons": { + "name": "دوست فیڈ مینو بٹنز", + "description": "دوست فیڈ مینو بار میں دکھائی جانے والے بٹنز منتخب کریں" + }, + "friend_feed_menu_position": { + "name": "دوست فیڈ مقام انڈیکس", + "description": "دوست فیڈ مینو کمپوننٹ کی مقام" + }, + "enable_friend_feed_menu_bar": { + "name": "نئے دوست فیڈ مینو بار کو فعال کریں", + "description": "نئے دوست فیڈ مینو بار کو فعال کرتا ہے" + } + } + }, + "messaging": { + "name": "میسیجنگ", + "description": "دوستوں کے ساتھ بات چیت کرنے کے طریقے تبدیل کریں", + "properties": { + "anonymous_story_viewing": { + "name": "غیر شناختہ کہانی دیکھنا", + "description": "کسی کو بھی آپ کی کہانی دیکھی ہونے کا علم نہیں ہونے دیتا" + }, + "hide_bitmoji_presence": { + "name": "بٹ موجی موجودگی چھپائیں", + "description": "آپ کے بٹ موجی کو چیٹ کے دوران دکھنے سے روکتا ہے" + }, + "hide_typing_notifications": { + "name": "ٹائپنگ اطلاعات چھپائیں", + "description": "کسی کو بھی آپ کی پیغام ٹائپ کرنے کا علم نہیں ہونے دیتا" + }, + "unlimited_snap_view_time": { + "name": "غیر محدود اسنیپ دیکھنے کا وقت", + "description": "سنیپس دیکھنے کیلئے وقت کی پابندی کو دور کرتا ہے" + }, + "disable_replay_in_ff": { + "name": "دوستوں کی فیڈ فرمیں دوبارہ چلانے کی غیر فعالیت", + "description": "دوستوں کی فیڈ سے لمبی دباؤ کے ساتھ دوبارہ چلانے کی صلاحیت کو منسوخ کرتا ہے" + }, + "message_preview_length": { + "name": "پیغام کی پیش نظر لمبائی", + "description": "پیغامات کی پیش نظر کرنے والی پیغامات کی مقدار معین کریں" + }, + "prevent_message_sending": { + "name": "پیغام بھیجنے سے روکنا", + "description": "مخصوص اقسام کے پیغامات بھیجنے سے روکتا ہے" + }, + "better_notifications": { + "name": "بہترین اطلاعات", + "description": "موصول ہونے والی اطلاعات میں مزید معلومات شامل کرتا ہے" + }, + "notification_blacklist": { + "name": "اطلاعات کی سیاہ فہرست", + "description": "روکنے چاہیں والی اطلاعات منتخب کریں" + }, + "message_logger": { + "name": "پیغام لاگر", + "description": "پیغامات کو مٹانے سے روکتا ہے" + }, + "auto_save_messages_in_conversations": { + "name": "پیغامات خود بخود محفوظ کریں", + "description": "بات چیتوں میں ہر پیغام خود بخود محفوظ کرتا ہے" + }, + "gallery_media_send_override": { + "name": "گیلری کے میڈیا بھیجنے کی بدلنے کی سیٹنگ", + "description": "گیلری سے بھیجنے کے وقت میڈیا کی ماخذ کو بدل دیتا ہے" + } + } + }, + "global": { + "name": "عالمی", + "description": "عالمی سنیپ چیٹ کی ترتیبات تبدیل کریں", + "properties": { + "spoofLocation": { + "name": "مقام", + "description": "آپ کا مقام فریب کریں", + "properties": { + "coordinates": { + "name": "کوآرڈینیٹس", + "description": "کوآرڈینیٹس تعین کریں" + } + } + }, + "snapchat_plus": { + "name": "سنیپ چیٹ پلس", + "description": "سنیپ چیٹ پلس فیچرز کو فعال کرتا ہے\nکچھ سرور-سائیڈ فیچرز ممکن ہے کام نہ کریں" + }, + "auto_updater": { + "name": "آٹو اپ ڈیٹر", + "description": "نئی اپ ڈیٹس کے لئے خود بخود چیک کرتا ہے" + }, + "disable_metrics": { + "name": "میٹرکس کو منسوخ کریں", + "description": "خصوصی تجزیاتی معلومات کو سنیپ چیٹ کو ارسال کرنے سے روکتا ہے" + }, + "block_ads": { + "name": "اشتہارات کو روکیں", + "description": "اشتہارات کو نمائش کرنے سے روکتا ہے" + }, + "bypass_video_length_restriction": { + "name": "ویڈیو کی لمبائی کی پابندی کو اندر کریں", + "description": "اکیلا: ایک ویڈیو بھیجتا ہے\nسپلٹ: ترتیب دینے کے بعد ویڈیو کو تقسیم کرتا ہے" + }, + "disable_google_play_dialogs": { + "name": "گوگل پلے سروسز ڈائیلاگ کو منسوخ کریں", + "description": "گوگل پلے سروسز کی دستیابی ڈائیلاگ کو نمائش کرنے سے روکتا ہے" + }, + "disable_snap_splitting": { + "name": "اسنیپ کو تقسیم کرنے سے روکیں", + "description": "سنیپس کو مختلف حصوں میں تقسیم ہونے سے روکتا ہے\nآپ کی بھیجی گئی تصاویر ویڈیوز میں تبدیل ہوجائیں گی" + } + } + }, + "rules": { + "name": "قواعد", + "description": "انفرادی لوگوں کے لئے خود کار فیچرز کو منظم کریں" + }, + "camera": { + "name": "کیمرا", + "description": "مکمل اسنیپ کیلئے درست ترتیبات کو ترتیب دیں", + "properties": { + "disable_camera": { + "name": "کیمرا کو منسوخ کریں", + "description": "سنیپ چیٹ کو آپ کی ڈوائس پر موجود کیمروں کا استعمال نہیں کرنے دیتا" + }, + "immersive_camera_preview": { + "name": "محیط پیش نظر", + "description": "سنیپ چیٹ کو کیمرے کی پیش نظر کو کاٹنے سے روکتا ہے\nیہ کچھ ڈیوائسز پر کیمرا کو بلبلی کرنے کی بنیاد پر ہو سکتا ہے" + }, + "override_preview_resolution": { + "name": "پیش نظر کی قرار داد کو بدلیں", + "description": "کیمرا پیش نظر کی قرار داد کو بدل دیتا ہے" + }, + "override_picture_resolution": { + "name": "تصویر کی قرار داد کو بدلیں", + "description": "تصویر کی قرار داد کو بدل دیتا ہے" + }, + "custom_frame_rate": { + "name": "خود مخصوص فریم ریٹ", + "description": "کیمرا فریم ریٹ کو بدل دیتا ہے" + }, + "force_camera_source_encoding": { + "name": "کیمرا کی ماخذ کو کواشن کریں", + "description": "کیمرا کی ماخذ کو کواشن کرتا ہے" + } + } + }, + "streaks_reminder": { + "name": "پکڑو میٹھا معلومات", + "description": "آپ کو دورانیہ بھر میں آپ کے پکڑو میٹھے کے بارے میں معلوم کرتا ہے", + "properties": { + "interval": { + "name": "دوری", + "description": "ہر معلومات کے درمیان وقفہ (گھنٹے)" + }, + "remaining_hours": { + "name": "باقی وقت", + "description": "معلومات کے دکھائی جانے سے پہلے باقی معمول کا وقت" + }, + "group_notifications": { + "name": "گروپ معلومات", + "description": "معلومات کو ایک میں گروپ کرتا ہے" + } + } + }, + "experimental": { + "name": "تجرباتی", + "description": "تجرباتی فیچرز", + "properties": { + "native_hooks": { + "name": "نیٹو ہکس", + "description": "سنیپ چیٹ کے نیٹو کوڈ میں چھپتے بے حسن فیچرز", + "properties": { + "disable_bitmoji": { + "name": "غیرفعال بٹ موجی", + "description": "دوست کی پروفائل بٹ موجی غیرفعال کریں" + } + } + }, + "spoof": { + "name": "فریب", + "description": "آپ کے بارے میں مختلف معلومات کو فریب دیتا ہے" + }, + "app_passcode": { + "name": "ایپ پاس کوڈ", + "description": "ایپ کو منظم کرنے کے لئے ایک پاس کوڈ تعین کرتا ہے" + }, + "app_lock_on_resume": { + "name": "ریزوم پر ایپ لاک", + "description": "ایپ کو دوبارہ کھولنے پر ایپ کو لاک کرتا ہے" + }, + "infinite_story_boost": { + "name": "بے انتہا کہانی کی بوسٹ", + "description": "کہانی کی بوسٹ لمبائی کی پابندی کو چھوڑ دیتا ہے" + }, + "meo_passcode_bypass": { + "name": "مائی آئز اونلی پاس کوڈ کو چھوڑ دیں", + "description": "مائی آئز اونلی پاس کوڈ کو چھوڑتا ہے\nیہ صرف اس صورت کام کرتا ہے اگر پاس کوڈ کو درست طریقے سے داخل کیا گیا ہو" + }, + "unlimited_multi_snap": { + "name": "بے انتہا ملٹی سنیپ", + "description": "آپ کو بے انتہا تعداد میں ملٹی سنیپس لینے کی اجازت دیتا ہے" + }, + "no_friend_score_delay": { + "name": "دوست کے امتیاز کی دیری کو روکیں", + "description": "ایک دوست کے امتیاز کو دیکھنے کے دوران کی تاخیر کو ہٹاتا ہے" + }, + "e2ee": { + "name": "اینڈ-ٹو-اینڈ انکرپشن", + "description": "AES کا استعمال کرتے ہوئے آپ کے پیغامات کو انکرپٹ کرتا ہے ایک مشترک رازی کلید کا استعمال کرتے ہوئے\nیقینی بنائیں کہ آپ اپنے کلید کو کہیں محفوظ رکھتے ہیں!", + "properties": { + "encrypted_message_indicator": { + "name": "انکرپٹ پیغام کی نمائش", + "description": "انکرپٹ پیغام کے ساتھ ایک 🔒 ایموجی شامل کرتا ہے" + }, + "force_message_encryption": { + "name": "پیغام انکرپشن کو زبردستی کریں", + "description": "انکرپٹ پیغامات کو انکرپشن فعال کرنے والے لوگوں کو بھیجنے سے روکتا ہے، صرف جب کئی مواد منتخب کرے" + } + } + }, + "add_friend_source_spoof": { + "name": "دوست کی درخواست کا سورس فریب", + "description": "دوست کی درخواست کا ماخذ فریب دیتا ہے" + }, + "hidden_snapchat_plus_features": { + "name": "مخفی سنیپ چیٹ پلس فیچرز", + "description": "غیر جاری/بیٹا سنیپ چیٹ پلس فیچرز کو فعال کرتا ہے\nپرانے سنیپ چیٹ ورژنز پر کام نہیں کر سکتا" + } + } + }, + "scripting": { + "name": "سکرپٹنگ", + "description": "کسٹم اسکرپٹس کو انسٹال کرنے کے لئے سنیپ اینہانس کو توسیع دینے کے لئے چلائیں", + "properties": { + "developer_mode": { + "name": "ڈویلپر موڈ", + "description": "سنیپ چیٹ کی یو آئی پر ڈیبگ انفو دکھاتا ہے" + }, + "module_folder": { + "name": "ماڈیول فولڈر", + "description": "اسکرپٹس کی جگہ کا فولڈر" + } + } + } + }, + "options": { + "app_appearance": { + "always_light": "ہمیشہ روشن", + "always_dark": "ہمیشہ اندھیرا" + }, + "better_notifications": { + "reply_button": "رد کا بٹن شامل کریں", + "download_button": "ڈاؤن لوڈ بٹن شامل کریں", + "group": "گروپ اطلاعات" + }, + "friend_feed_menu_buttons": { + "auto_download": "⬇️ خود بخود ڈاؤن لوڈ", + "auto_save": "💬 خود بخود پیغام محفوظ کریں", + "stealth": "👻 چھپنے والا موڈ", + "conversation_info": "👤 مکالمہ کی معلومات", + "e2e_encryption": "🔒 اینڈ-ٹو-اینڈ انکرپشن کا استعمال کریں" + }, + "path_format": { + "create_author_folder": "ہر مصنف کے لئے فولڈر بنائیں", + "create_source_folder": "ہر میڈیا سورس کیلئے فولڈر بنائیں", + "append_hash": "فائل کے نام میں ایک یونیک ہیش شامل کریں", + "append_source": "فائل کے نام میں میڈیا سورس شامل کریں", + "append_username": "فائل کے نام میں صارف کا نام شامل کریں", + "append_date_time": "فائل کے نام میں تاریخ اور وقت شامل کریں" + }, + "auto_download_sources": { + "friend_snaps": "دوست کی سنیپس", + "friend_stories": "دوست کی کہانیاں", + "public_stories": "عوامی کہانیاں", + "spotlight": "سپاٹ لائٹ" + }, + "logging": { + "started": "شروع ہوگیا", + "success": "کامیابی", + "progress": "پیش رفت", + "failure": "ناکامی" + }, + "notifications": { + "chat_screenshot": "اسکرین شاٹ", + "chat_screen_record": "سکرین ریکارڈ", + "snap_replay": "سنیپ دوبارہ دیکھیں", + "camera_roll_save": "کیمرہ رول میں محفوظ کریں", + "chat": "چیٹ", + "chat_reply": "چیٹ جواب", + "snap": "سنیپ", + "typing": "ٹائپنگ", + "stories": "کہانیاں", + "chat_reaction": "ڈی ایم ری ایکشن", + "group_chat_reaction": "گروپ ری ایکشن", + "initiate_audio": "آنے والی آڈیو کال", + "abandon_audio": "چھوڑی گئی آڈیو کال", + "initiate_video": "آنے والی ویڈیو کال", + "abandon_video": "چھوڑی گئی ویڈیو کال" + }, + "gallery_media_send_override": { + "ORIGINAL": "اصل", + "NOTE": "آڈیو نوٹ", + "SNAP": "سنیپ", + "SAVABLE_SNAP": "محفوظ سنیپ" + }, + "hide_ui_components": { + "hide_profile_call_buttons": "پروفائل کال بٹنز ہٹائیں", + "hide_chat_call_buttons": "چیٹ کال بٹنز ہٹائیں", + "hide_live_location_share_button": "لاکھوں لوگوں کا سراغ لگانے کا بٹن ہٹائیں", + "hide_stickers_button": "اسٹکر بٹن ہٹائیں", + "hide_voice_record_button": "آواز ریکارڈ بٹن ہٹائیں" + }, + "hide_story_sections": { + "hide_friend_suggestions": "دوست کی تجاویز چھپائیں", + "hide_friends": "دوستوں کو چھپائیں", + "hide_suggested": "مشورے شدہ حصے کو چھپائیں", + "hide_for_you": "آپ کے لئے حصے کو چھپائیں" + }, + "home_tab": { + "map": "نقشہ", + "chat": "چیٹ", + "camera": "کیمرا", + "discover": "ڈسکور", + "spotlight": "سپاٹ لائٹ" + }, + "add_friend_source_spoof": { + "added_by_username": "صارف کے ذریعے", + "added_by_mention": "ذکر کے ذریعے", + "added_by_group_chat": "گروپ چیٹ کے ذریعے", + "added_by_qr_code": "کیو آر کوڈ کے ذریعے", + "added_by_community": "کمیونٹی کے ذریعے" + }, + "bypass_video_length_restriction": { + "single": "ایکل میڈیا", + "split": "میڈیا کو تقسیم کریں" + } + } + }, + "friend_menu_option": { + "preview": "پیش نظارہ", + "stealth_mode": "چھپائیں وضع", + "auto_download_blacklist": "خود بخود ڈاؤن لوڈ بلیک لسٹ", + "anti_auto_save": "ضد خود بخود محفوظ کریں" + }, + "chat_action_menu": { + "preview_button": "پیش نظارہ", + "download_button": "ڈاؤن لوڈ", + "delete_logged_message_button": "لاگ کردہ پیغام کو حذف کریں" + }, + "opera_context_menu": { + "download": "میڈیا ڈاؤن لوڈ کریں" + }, + "modal_option": { + "profile_info": "پروفائل کی معلومات", + "close": "بند کریں" + }, + "gallery_media_send_override": { + "multiple_media_toast": "آپ صرف ایک میڈیا ایک وقت پر بھیج سکتے ہیں" + }, + "conversation_preview": { + "streak_expiration": "{day} دن {hour} گھنٹے {minute} منٹ میں ختم ہوگا", + "total_messages": "جمع پیغامات: {count}", + "title": "پیش نظارہ", + "unknown_user": "نامعلوم صارف" + }, + "profile_info": { + "title": "پروفائل کی معلومات", + "first_created_username": "پہلا تخلیق شدہ صارف نام", + "mutable_username": "قابل تبدیل صارف نام", + "display_name": "نمائشی نام", + "added_date": "شامل کرنے کی تاریخ", + "birthday": "تاریخ پیدائش: {month} {day}", + "friendship": "دوستی", + "add_source": "شامل کرنے کی سرسراہی", + "snapchat_plus": "سنیپ چیٹ پلس", + "snapchat_plus_state": { + "subscribed": "مشترک", + "not_subscribed": "غیر مشترک" + } + }, + "chat_export": { + "dialog_negative_button": "منسوخ کریں", + "dialog_positive_button": "برآمد کریں", + "exported_to": "{path} کو برآمد کیا گیا", + "exporting_chats": "مکالمات کو برآمد کر رہا ہے...", + "processing_chats": "{amount} مکالمات کو پروسیس کر رہا ہے...", + "export_fail": "مکالمہ برآمد کرنے میں ناکامی {conversation}", + "writing_output": "آؤٹپٹ لکھ رہا ہے...", + "finished": "ہوگیا! آپ اب اس ڈائیلاگ کو بند کر سکتے ہیں۔", + "no_messages_found": "کوئی پیغام نہیں ملا!", + "exporting_message": "{conversation} کو برآمد کر رہا ہے..." + }, + "button": { + "ok": "ٹھیک ہے", + "positive": "ہاں", + "negative": "نہیں", + "cancel": "منسوخ کریں", + "open": "کھولیں", + "download": "ڈاؤن لوڈ کریں" + }, + "profile_picture_downloader": { + "button": "پروفائل تصویر ڈاؤن لوڈ کریں", + "title": "پروفائل تصویر ڈاؤن لوڈ کرنے والا", + "avatar_option": "آوازار", + "background_option": "پس منظر" + }, + "download_processor": { + "attachment_type": { + "snap": "اسٹینگ", + "sticker": "اسٹکر", + "external_media": "بیرونی میڈیا", + "note": "نوٹ", + "original_story": "اصل کہانی" + }, + "select_attachments_title": "ڈاؤن لوڈ کرنے کے لئے منسلکمنٹ منتخب کریں", + "download_started_toast": "ڈاؤن لوڈ شروع ہوگیا", + "unsupported_content_type_toast": "غیر معاون مواد کی قسم!", + "failed_no_longer_available_toast": "میڈیا دستیاب نہیں ہے", + "no_attachments_toast": "کوئی منسلک منسلکمنٹ نہیں ملا!", + "already_queued_toast": "میڈیا پہلے ہی قیو ہے!", + "already_downloaded_toast": "میڈیا پہلے ہی ڈاؤن لوڈ ہوگیا ہے!", + "saved_toast": "{path} میں محفوظ ہوگیا", + "download_toast": "{path} ڈاؤن لوڈ ہورہا ہے...", + "processing_toast": "{path} پروسیس ہورہا ہے...", + "failed_generic_toast": "ڈاؤن لوڈ کرنے میں ناکامی", + "failed_to_create_preview_toast": "پیش نظارہ تخلیق کرنے میں ناکامی", + "failed_processing_toast": "پروسیس کرنے میں ناکامی {error}", + "failed_gallery_toast": "گیلری میں محفوظ کرنے میں ناکامی {error}" + }, + "streaks_reminder": { + "notification_title": "مسلسلی", + "notification_text": "آپ {hoursLeft} گھنٹوں میں اپنے دوست {friend} کے ساتھ اپنے مسلسلی کو خوو دیں گے" + } +} diff --git a/common/src/main/assets/lang/zh_SIMPLIFIED.json b/common/src/main/assets/lang/zh_SIMPLIFIED.json new file mode 100644 index 000000000..db28d4c01 --- /dev/null +++ b/common/src/main/assets/lang/zh_SIMPLIFIED.json @@ -0,0 +1,55 @@ +{ + "setup": { + "dialogs": { + "select_language": "选择语言" + } + }, + "friend_menu_option": { + "preview": "预览", + "stealth_mode": "隐身模式", + "auto_download_blacklist": "自动下载黑名单", + "anti_auto_save": "自动保存" + }, + "chat_action_menu": { + "preview_button": "预览", + "download_button": "下载", + "delete_logged_message_button": "删除已记录的消息" + }, + "opera_context_menu": { + "download": "下载媒体" + }, + "modal_option": { + "profile_info": "配置信息", + "close": "关闭" + }, + "conversation_preview": { + "streak_expiration": "在 {day} 天 {hour} 小时 {minute} 分钟", + "title": "预览", + "unknown_user": "未知用户" + }, + "profile_info": { + "title": "配置信息", + "display_name": "显示姓名 ", + "added_date": "添加日期", + "birthday": "生日: {month} {day}" + }, + "chat_export": { + "dialog_negative_button": "取消", + "dialog_positive_button": "导出", + "exported_to": "导出到 {path}", + "exporting_chats": "正在导出聊天...", + "processing_chats": "正在处理 {amount} 个对话...", + "export_fail": "导出对话 {conversation} 失败", + "writing_output": "正在写入输出...", + "finished": "完成了!您现在可以关闭此对话框。", + "no_messages_found": "未找到消息!", + "exporting_message": "正在导出 {conversation}..." + }, + "button": { + "ok": "确定", + "positive": "是", + "negative": "否", + "cancel": "取消", + "open": "打开" + } +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMetrics.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMetrics.kt index be532061f..23cd1a30c 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMetrics.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMetrics.kt @@ -15,4 +15,4 @@ class DisableMetrics : Feature("DisableMetrics", loadParams = FeatureLoadParams. } } } -} \ No newline at end of file +} From d1c4b4febeb8300fa7fbf6adb762134ba415a8f5 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:56:27 +0100 Subject: [PATCH 37/53] fix(app): two letters locale --- .../ui/manager/sections/home/HomeSection.kt | 12 ++++++++---- .../ui/setup/screens/impl/PickLanguageScreen.kt | 5 +++-- .../snapenhance/common/bridge/types/LocalePair.kt | 12 +++++++++++- .../common/bridge/wrapper/LocaleWrapper.kt | 6 +++--- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt index 58e38a55a..4c7dca85d 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt @@ -211,7 +211,7 @@ class HomeSection : Section() { ) Text( - text = arrayOf("\u0020", "\u0065", "\u0063", "\u006e", "\u0061", "\u0068", "\u006e", "\u0045", "\u0070", "\u0061", "\u006e", "\u0053").reversed().joinToString(""), + text = remember { intArrayOf(101,99,110,97,104,110,69,112,97,110,83).map { it.toChar() }.joinToString("").reversed() }, fontSize = 30.sp, fontFamily = avenirNextFontFamily, modifier = Modifier.align(Alignment.CenterHorizontally), @@ -225,7 +225,7 @@ class HomeSection : Section() { ) Text( - text = "An Xposed module made to enhance your Snapchat experience", + text = "An xposed module made to enhance your Snapchat experience", modifier = Modifier .padding(16.dp) .fillMaxWidth(), @@ -245,7 +245,9 @@ class HomeSection : Section() { modifier = Modifier.size(32.dp).clickable { context.activity?.startActivity( Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse("https://github.com/rhunk/SnapEnhance") + data = Uri.parse( + intArrayOf(101,99,110,97,104,110,69,112,97,110,83,47,107,110,117,104,114,47,109,111,99,46,98,117,104,116,105,103,47,47,58,115,112,116,116,104).map { it.toChar() }.joinToString("").reversed() + ) flags = Intent.FLAG_ACTIVITY_NEW_TASK } ) @@ -258,7 +260,9 @@ class HomeSection : Section() { modifier = Modifier.size(32.dp).clickable { context.activity?.startActivity( Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse("https://t.me/snapenhance") + data = Uri.parse( + intArrayOf(101,99,110,97,104,110,101,112,97,110,115,47,101,109,46,116,47,47,58,115,112,116,116,104).map { it.toChar() }.joinToString("").reversed() + ) flags = Intent.FLAG_ACTIVITY_NEW_TASK } ) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt index f477de72a..c6b5a1c10 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt @@ -40,6 +40,7 @@ class PickLanguageScreen : SetupScreen(){ private fun getLocaleDisplayName(locale: String): String { locale.split("_").let { + if (it.size != 2) return Locale(locale).getDisplayName(Locale.getDefault()) return Locale(it[0], it[1]).getDisplayName(Locale.getDefault()) } } @@ -105,7 +106,7 @@ class PickLanguageScreen : SetupScreen(){ contentAlignment = Alignment.Center ) { Text( - text = getLocaleDisplayName(locale), + text = remember(locale) { getLocaleDisplayName(locale) }, fontSize = 16.sp, fontWeight = FontWeight.Light, ) @@ -125,7 +126,7 @@ class PickLanguageScreen : SetupScreen(){ Button(onClick = { isDialog = true }) { - Text(text = getLocaleDisplayName(selectedLocale.value), fontSize = 16.sp, + Text(text = remember(selectedLocale.value) { getLocaleDisplayName(selectedLocale.value) }, fontSize = 16.sp, fontWeight = FontWeight.Normal) } } diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/LocalePair.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/LocalePair.kt index 096f2d534..2c0f6647e 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/LocalePair.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/LocalePair.kt @@ -1,6 +1,16 @@ package me.rhunk.snapenhance.common.bridge.types +import java.util.Locale + data class LocalePair( val locale: String, val content: String -) \ No newline at end of file +) { + fun getLocale(): Locale { + if (locale.contains("_")) { + val split = locale.split("_") + return Locale(split[0], split[1]) + } + return Locale(locale) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LocaleWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LocaleWrapper.kt index 4eed2a567..16ee7ec6d 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LocaleWrapper.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LocaleWrapper.kt @@ -19,7 +19,7 @@ class LocaleWrapper { if (locale == DEFAULT_LOCALE) return locales - val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(locale) }?.substring(0, 5) ?: return locales + val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(locale) }?.substringBefore(".") ?: return locales context.resources.assets.open("lang/$compatibleLocale.json").use { inputStream -> locales.add(LocalePair(compatibleLocale, inputStream.bufferedReader().use { it.readText() })) @@ -29,7 +29,7 @@ class LocaleWrapper { } fun fetchAvailableLocales(context: Context): List { - return context.resources.assets.list("lang")?.map { it.substring(0, 5) } ?: listOf() + return context.resources.assets.list("lang")?.map { it.substringBefore(".") }?.sorted() ?: listOf(DEFAULT_LOCALE) } } @@ -40,7 +40,7 @@ class LocaleWrapper { lateinit var loadedLocale: Locale private fun load(localePair: LocalePair) { - loadedLocale = localePair.locale.let { Locale(it.substring(0, 2), it.substring(3, 5)) } + loadedLocale = localePair.getLocale() val translations = JsonParser.parseString(localePair.content).asJsonObject if (translations == null || translations.isJsonNull) { From a90f4875a73a7ae1a985be183bf51f3bfd4a3d75 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 4 Jan 2024 23:09:27 +0100 Subject: [PATCH 38/53] fix(database): db cache --- .../manager/sections/social/ScopeContent.kt | 108 +++++++++-------- .../core/action/impl/ExportMemories.kt | 2 +- .../core/database/DatabaseAccess.kt | 114 +++++++++--------- 3 files changed, 116 insertions(+), 108 deletions(-) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt index ef7566b59..3e58012c6 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt @@ -261,69 +261,71 @@ class ScopeContent( Spacer(modifier = Modifier.height(16.dp)) // e2ee section - SectionTitle(translation["e2ee_title"]) - var hasSecretKey by remember { mutableStateOf(context.e2eeImplementation.friendKeyExists(friend.userId))} - var importDialog by remember { mutableStateOf(false) } + if (context.config.root.experimental.e2eEncryption.globalState == true) { + SectionTitle(translation["e2ee_title"]) + var hasSecretKey by remember { mutableStateOf(context.e2eeImplementation.friendKeyExists(friend.userId))} + var importDialog by remember { mutableStateOf(false) } - if (importDialog) { - Dialog( - onDismissRequest = { importDialog = false } - ) { - dialogs.RawInputDialog(onDismiss = { importDialog = false }, onConfirm = { newKey -> - importDialog = false - runCatching { - val key = Base64.decode(newKey) - if (key.size != 32) { - context.longToast("Invalid key size (must be 32 bytes)") - return@runCatching - } + if (importDialog) { + Dialog( + onDismissRequest = { importDialog = false } + ) { + dialogs.RawInputDialog(onDismiss = { importDialog = false }, onConfirm = { newKey -> + importDialog = false + runCatching { + val key = Base64.decode(newKey) + if (key.size != 32) { + context.longToast("Invalid key size (must be 32 bytes)") + return@runCatching + } - context.e2eeImplementation.storeSharedSecretKey(friend.userId, key) - context.longToast("Successfully imported key") - hasSecretKey = true - }.onFailure { - context.longToast("Failed to import key: ${it.message}") - context.log.error("Failed to import key", it) - } - }) + context.e2eeImplementation.storeSharedSecretKey(friend.userId, key) + context.longToast("Successfully imported key") + hasSecretKey = true + }.onFailure { + context.longToast("Failed to import key: ${it.message}") + context.log.error("Failed to import key", it) + } + }) + } } - } - ContentCard { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) - ) { - if (hasSecretKey) { - OutlinedButton(onClick = { - val secretKey = Base64.encode(context.e2eeImplementation.getSharedSecretKey(friend.userId) ?: return@OutlinedButton) - //TODO: fingerprint auth - context.activity!!.startActivity(Intent.createChooser(Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, secretKey) - type = "text/plain" - }, "").apply { - putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf( - Intent().apply { - putExtra(Intent.EXTRA_TEXT, secretKey) - putExtra(Intent.EXTRA_SUBJECT, secretKey) - }) + ContentCard { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + if (hasSecretKey) { + OutlinedButton(onClick = { + val secretKey = Base64.encode(context.e2eeImplementation.getSharedSecretKey(friend.userId) ?: return@OutlinedButton) + //TODO: fingerprint auth + context.activity!!.startActivity(Intent.createChooser(Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, secretKey) + type = "text/plain" + }, "").apply { + putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf( + Intent().apply { + putExtra(Intent.EXTRA_TEXT, secretKey) + putExtra(Intent.EXTRA_SUBJECT, secretKey) + }) + ) + }) + }) { + Text( + text = "Export Base64", + maxLines = 1 ) - }) - }) { + } + } + + OutlinedButton(onClick = { importDialog = true }) { Text( - text = "Export Base64", + text = "Import Base64", maxLines = 1 ) } } - - OutlinedButton(onClick = { importDialog = true }) { - Text( - text = "Import Base64", - maxLines = 1 - ) - } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportMemories.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportMemories.kt index adfb586b6..e7d33803d 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportMemories.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportMemories.kt @@ -363,7 +363,7 @@ class ExportMemories : AbstractAction() { val database = runCatching { SQLiteDatabase.openDatabase( context.androidContext.getDatabasePath("memories.db"), - OpenParams.Builder().build(), + OpenParams.Builder().setOpenFlags(SQLiteDatabase.OPEN_READONLY).build() ) }.getOrNull() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt index 625c987ce..5596f553c 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt @@ -19,17 +19,45 @@ import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.manager.Manager +enum class DatabaseType( + val fileName: String +) { + MAIN("main.db"), + ARROYO("arroyo.db") +} + class DatabaseAccess( private val context: ModContext ) : Manager { - companion object { - val DATABASES = mapOf( - "main" to "main.db", - "arroyo" to "arroyo.db" - ) + private val openedDatabases = mutableMapOf() + + private fun useDatabase(database: DatabaseType, writeMode: Boolean = false): SQLiteDatabase? { + if (openedDatabases.containsKey(database) && openedDatabases[database]?.isOpen == true) { + return openedDatabases[database] + } + + val dbPath = context.androidContext.getDatabasePath(database.fileName) + if (!dbPath.exists()) return null + return runCatching { + SQLiteDatabase.openDatabase( + dbPath, + OpenParams.Builder() + .setOpenFlags( + if (writeMode) SQLiteDatabase.OPEN_READWRITE or SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING + else SQLiteDatabase.OPEN_READONLY + ) + .setErrorHandler { + context.androidContext.deleteDatabase(dbPath.absolutePath) + context.softRestartApp() + }.build() + ) + }.onFailure { + context.log.error("Failed to open database ${database.fileName}!", it) + }.getOrNull()?.also { + openedDatabases[database] = it + } } - private val mainDb by lazy { openLocalDatabase("main") } - private val arroyoDb by lazy { openLocalDatabase("arroyo") } + private inline fun SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? { return runCatching { @@ -56,7 +84,7 @@ class DatabaseAccess( } private val dmOtherParticipantCache by lazy { - (arroyoDb?.performOperation { + (useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT client_conversation_id, conversation_type, user_id FROM user_conversation WHERE user_id != ?", arrayOf(myUserId) @@ -79,49 +107,27 @@ class DatabaseAccess( } ?: emptyMap()).toMutableMap() } - private fun openLocalDatabase(databaseName: String, writeMode: Boolean = false): SQLiteDatabase? { - val dbPath = context.androidContext.getDatabasePath(DATABASES[databaseName]!!) - if (!dbPath.exists()) return null - return runCatching { - SQLiteDatabase.openDatabase( - dbPath, - OpenParams.Builder() - .setOpenFlags( - if (writeMode) SQLiteDatabase.OPEN_READWRITE or SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING - else SQLiteDatabase.OPEN_READONLY - ) - .setErrorHandler { - context.androidContext.deleteDatabase(dbPath.absolutePath) - context.softRestartApp() - }.build() - ) - }.onFailure { - context.log.error("Failed to open database $databaseName!", it) - }.getOrNull() - } - - fun hasMain(): Boolean = mainDb?.isOpen == true - fun hasArroyo(): Boolean = arroyoDb?.isOpen == true + fun hasMain(): Boolean = useDatabase(DatabaseType.MAIN)?.isOpen == true + fun hasArroyo(): Boolean = useDatabase(DatabaseType.ARROYO)?.isOpen == true override fun init() { // perform integrity check on databases - DATABASES.forEach { (name, fileName) -> - openLocalDatabase(name, writeMode = true)?.apply { + DatabaseType.entries.forEach { type -> + useDatabase(type, writeMode = true)?.apply { rawQuery("PRAGMA integrity_check", null).use { query -> if (!query.moveToFirst() || query.getString(0).lowercase() != "ok") { - context.log.error("Failed to perform integrity check on $fileName") - context.androidContext.deleteDatabase(fileName) + context.log.error("Failed to perform integrity check on ${type.fileName}") + context.androidContext.deleteDatabase(type.fileName) return@apply } - context.log.verbose("database $fileName integrity check passed") + context.log.verbose("database ${type.fileName} integrity check passed") } }?.close() } } fun finalize() { - mainDb?.close() - arroyoDb?.close() + openedDatabases.values.forEach { it.close() } context.log.verbose("Database closed") } @@ -143,7 +149,7 @@ class DatabaseAccess( } fun getFeedEntryByUserId(userId: String): FriendFeedEntry? { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { readDatabaseObject( FriendFeedEntry(), "FriendsFeedView", @@ -155,7 +161,7 @@ class DatabaseAccess( val myUserId by lazy { context.androidContext.getSharedPreferences("user_session_shared_pref", 0).getString("key_user_id", null) ?: - arroyoDb?.performOperation { + useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery(buildString { append("SELECT value FROM required_values WHERE key = 'USERID'") }, null)?.use { query -> @@ -168,7 +174,7 @@ class DatabaseAccess( } fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { readDatabaseObject( FriendFeedEntry(), "FriendsFeedView", @@ -179,7 +185,7 @@ class DatabaseAccess( } fun getFriendInfo(userId: String): FriendInfo? { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { readDatabaseObject( FriendInfo(), "FriendWithUsername", @@ -190,7 +196,7 @@ class DatabaseAccess( } fun getAllFriends(): List { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { safeRawQuery( "SELECT * FROM FriendWithUsername", null @@ -209,7 +215,7 @@ class DatabaseAccess( } fun getFeedEntries(limit: Int): List { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { safeRawQuery( "SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?", arrayOf(limit.toString()) @@ -228,7 +234,7 @@ class DatabaseAccess( } fun getConversationMessageFromId(clientMessageId: Long): ConversationMessage? { - return arroyoDb?.performOperation { + return useDatabase(DatabaseType.ARROYO)?.performOperation { readDatabaseObject( ConversationMessage(), "conversation_message", @@ -239,7 +245,7 @@ class DatabaseAccess( } fun getConversationType(conversationId: String): Int? { - return arroyoDb?.performOperation { + return useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT conversation_type FROM user_conversation WHERE client_conversation_id = ?", arrayOf(conversationId) @@ -253,7 +259,7 @@ class DatabaseAccess( } fun getConversationLinkFromUserId(userId: String): UserConversationLink? { - return arroyoDb?.performOperation { + return useDatabase(DatabaseType.ARROYO)?.performOperation { readDatabaseObject( UserConversationLink(), "user_conversation", @@ -265,7 +271,7 @@ class DatabaseAccess( fun getDMOtherParticipant(conversationId: String): String? { if (dmOtherParticipantCache.containsKey(conversationId)) return dmOtherParticipantCache[conversationId] - return arroyoDb?.performOperation { + return useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT user_id FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0", arrayOf(conversationId) @@ -284,14 +290,14 @@ class DatabaseAccess( fun getStoryEntryFromId(storyId: String): StoryEntry? { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { readDatabaseObject(StoryEntry(), "Story", "storyId = ?", arrayOf(storyId)) } } fun getConversationParticipants(conversationId: String): List? { if (dmOtherParticipantCache[conversationId] != null) return dmOtherParticipantCache[conversationId]?.let { listOf(myUserId, it) } - return arroyoDb?.performOperation { + return useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT user_id, conversation_type FROM user_conversation WHERE client_conversation_id = ?", arrayOf(conversationId) @@ -321,7 +327,7 @@ class DatabaseAccess( conversationId: String, limit: Int ): List? { - return arroyoDb?.performOperation { + return useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?", arrayOf(conversationId, limit.toString()) @@ -341,7 +347,7 @@ class DatabaseAccess( } fun getAddSource(userId: String): String? { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { rawQuery( "SELECT addSource FROM FriendWhoAddedMe WHERE userId = ?", arrayOf(userId) @@ -355,7 +361,7 @@ class DatabaseAccess( } fun markFriendStoriesAsSeen(userId: String) { - openLocalDatabase("main", writeMode = true)?.apply { + useDatabase(DatabaseType.MAIN, writeMode = true)?.apply { performOperation { execSQL("UPDATE StorySnap SET viewed = 1 WHERE userId = ?", arrayOf(userId)) } @@ -364,7 +370,7 @@ class DatabaseAccess( } fun getAccessTokens(userId: String): Map? { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { rawQuery( "SELECT accessTokensPb FROM SnapToken WHERE userId = ?", arrayOf(userId) From 477b13d3eb9767cc53d67f2be87b8427ba7fbf5e Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 4 Jan 2024 23:26:55 +0100 Subject: [PATCH 39/53] fix(core/export_chat_messages): no database cache --- .../rhunk/snapenhance/core/action/impl/ExportChatMessages.kt | 2 +- .../me/rhunk/snapenhance/core/database/DatabaseAccess.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportChatMessages.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportChatMessages.kt index 75f43ffe0..59cbc8a38 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportChatMessages.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportChatMessages.kt @@ -333,7 +333,7 @@ class ExportChatMessages : AbstractAction() { //first fetch the first message val conversationId = feedEntry.key!! val conversationName = feedEntry.feedDisplayName ?: feedEntry.friendDisplayName!!.split("|").lastOrNull() ?: "unknown" - val conversationParticipants = context.database.getConversationParticipants(feedEntry.key!!) + val conversationParticipants = context.database.getConversationParticipants(feedEntry.key!!, useCache = false) ?.mapNotNull { context.database.getFriendInfo(it) }?.associateBy { it.userId!! } ?: emptyMap() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt index 5596f553c..fc30967f9 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt @@ -295,8 +295,8 @@ class DatabaseAccess( } } - fun getConversationParticipants(conversationId: String): List? { - if (dmOtherParticipantCache[conversationId] != null) return dmOtherParticipantCache[conversationId]?.let { listOf(myUserId, it) } + fun getConversationParticipants(conversationId: String, useCache: Boolean = true): List? { + if (dmOtherParticipantCache[conversationId] != null && useCache) return dmOtherParticipantCache[conversationId]?.let { listOf(myUserId, it) } return useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT user_id, conversation_type FROM user_conversation WHERE client_conversation_id = ?", From aeaef6f440ce5a1fb1533596bde985efed2a50cd Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Fri, 5 Jan 2024 00:25:36 +0100 Subject: [PATCH 40/53] fix: date_range_input_title --- common/src/main/assets/lang/en_US.json | 1 - .../src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 80ab676fd..91558fe35 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -1082,7 +1082,6 @@ "switch_text": "Suspend Location Updates" }, "material3_strings": { - "date_range_input_title": "", "date_range_picker_start_headline": "From", "date_range_picker_end_headline": "To", "date_range_picker_title": "Select date range", diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt index 1ed311970..1a611c0dc 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -286,6 +286,11 @@ class SnapEnhance { Resources::class.java.getMethod("getString", Int::class.javaPrimitiveType).hook(HookStage.BEFORE) { param -> val key = param.arg(0) val name = stringResources[key] ?: return@hook + // FIXME: prevent blank string in translations + if (name == "date_range_input_title") { + param.setResult("") + return@hook + } param.setResult(appContext.translation.getOrNull("material3_strings.$name") ?: return@hook) } } From ba59af6dafbe254e1d4e4524ee0800cd58128971 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Fri, 5 Jan 2024 01:45:15 +0100 Subject: [PATCH 41/53] fix(app/social): rule switch text --- .../snapenhance/ui/manager/sections/social/ScopeContent.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt index 3e58012c6..a171a8a54 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt @@ -88,8 +88,7 @@ class ScopeContent( text = if (ruleType.listMode && ruleState != null) { context.translation["rules.properties.${ruleType.key}.options.${ruleState.key}"] } else context.translation["rules.properties.${ruleType.key}.name"], - maxLines = 1, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f).padding(start = 5.dp, end = 5.dp) ) Switch(checked = ruleEnabled, enabled = if (ruleType.listMode) ruleState != null else true, From eb81059f3e7ad695e410f841174547d123e080fc Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Fri, 5 Jan 2024 13:48:23 +0100 Subject: [PATCH 42/53] fix: ConvertMessageLocally --- .../impl/experiments/ConvertMessageLocally.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ConvertMessageLocally.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ConvertMessageLocally.kt index bb95d6c85..c828b834d 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ConvertMessageLocally.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ConvertMessageLocally.kt @@ -28,9 +28,16 @@ class ConvertMessageLocally : Feature("Convert Message Edit", loadParams = Featu fun convertMessageInterface(messageInstance: Message) { val actions = mutableMapOf Unit>() - actions["restore_original"] = { - messageCache.remove(it.messageDescriptor!!.messageId!!) - dispatchMessageEdit(it, restore = true) + actions["restore_original"] = actions@{ message -> + val descriptor = message.messageDescriptor ?: return@actions + messageCache.remove(descriptor.messageId!!) + context.feature(Messaging::class).conversationManager?.fetchMessage( + descriptor.conversationId!!.toString(), + descriptor.messageId!!, + onSuccess = { msg -> + dispatchMessageEdit(msg, true) + } + ) } val contentType = messageInstance.messageContent?.contentType From 8eeafc59b69cedba6978f4eafef909dae22fa185 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 7 Jan 2024 21:58:52 +0100 Subject: [PATCH 43/53] fix: FileType --- .../src/main/kotlin/me/rhunk/snapenhance/common/data/FileType.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/FileType.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/FileType.kt index 6f9672517..805cc465f 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/FileType.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/FileType.kt @@ -38,7 +38,6 @@ enum class FileType( "ffd8ff" to JPG, "47494638" to GIF, "1a45dfa3" to MKV, - "52494646" to AVI, ) fun fromString(string: String?): FileType { From 1f7f27076687166e5c608101cbbbde64ca2c6e6e Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 11 Jan 2024 18:09:16 +0100 Subject: [PATCH 44/53] feat: unaryCall event - feat data class builder util --- .../common/scripting/impl/Networking.kt | 7 +- .../common/util/protobuf/ProtoEditor.kt | 2 + .../snapenhance/core/event/EventDispatcher.kt | 63 ++++++++++++++++++ .../core/event/events/impl/UnaryCallEvent.kt | 14 ++++ .../snapenhance/core/util/DataClassBuilder.kt | 66 +++++++++++++++++++ .../impl/OperaPageViewControllerMapper.kt | 10 +-- 6 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/UnaryCallEvent.kt create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/util/DataClassBuilder.kt diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/Networking.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/Networking.kt index 2116fe46e..72caa1501 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/Networking.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/impl/Networking.kt @@ -5,6 +5,7 @@ import me.rhunk.snapenhance.common.scripting.bindings.BindingSide import me.rhunk.snapenhance.common.scripting.ktx.contextScope import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener @@ -32,13 +33,13 @@ class Networking : AbstractBinding("networking", BindingSide.COMMON) { fun removeHeader(name: String) = requestBuilder.removeHeader(name).let { this } @JSFunction - fun method(method: String, body: String) = requestBuilder.method(method, okhttp3.RequestBody.create(null, body)).let { this } + fun method(method: String, body: String) = requestBuilder.method(method, body.toRequestBody(null)).let { this } @JSFunction - fun method(method: String, body: java.io.InputStream) = requestBuilder.method(method, okhttp3.RequestBody.create(null, body.readBytes())).let { this } + fun method(method: String, body: java.io.InputStream) = requestBuilder.method(method, body.readBytes().toRequestBody(null)).let { this } @JSFunction - fun method(method: String, body: ByteArray) = requestBuilder.method(method, okhttp3.RequestBody.create(null, body)).let { this } + fun method(method: String, body: ByteArray) = requestBuilder.method(method, body.toRequestBody(null)).let { this } } inner class ResponseWrapper( diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/ProtoEditor.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/ProtoEditor.kt index dc8758b97..06f8e1f06 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/ProtoEditor.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/ProtoEditor.kt @@ -88,4 +88,6 @@ class ProtoEditor( } fun toByteArray() = buffer + + override fun toString() = ProtoReader(buffer).toString() } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt index 5cfba6db3..f0b2755fa 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt @@ -9,6 +9,7 @@ import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.event.events.impl.* import me.rhunk.snapenhance.core.manager.Manager import me.rhunk.snapenhance.core.util.hook.HookStage +import me.rhunk.snapenhance.core.util.hook.Hooker import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.ktx.getObjectField @@ -17,6 +18,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.MessageContent import me.rhunk.snapenhance.core.wrapper.impl.MessageDestinations import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import java.nio.ByteBuffer class EventDispatcher( private val context: ModContext @@ -156,6 +158,67 @@ class EventDispatcher( ) } + context.classCache.unifiedGrpcService.hook("unaryCall", HookStage.BEFORE) { param -> + val uri = param.arg(0) + val buffer = param.argNullable(1)?.run { + val array = ByteArray(limit()) + position(0) + get(array) + rewind() + array + } ?: return@hook + val unaryEventHandler = param.argNullable(3) ?: return@hook + + val event = context.event.post( + UnaryCallEvent( + uri = uri, + buffer = buffer + ).apply { + adapter = param + } + ) ?: return@hook + + if (event.canceled) { + param.setResult(null) + return@hook + } + + if (!event.buffer.contentEquals(buffer)) { + param.setArg(1, ByteBuffer.wrap(event.buffer)) + } + + if (event.callbacks.size == 0) { + return@hook + } + + Hooker.ephemeralHookObjectMethod(unaryEventHandler::class.java, unaryEventHandler, "onEvent", HookStage.BEFORE) { methodParam -> + val byteBuffer = methodParam.argNullable(0) ?: return@ephemeralHookObjectMethod + val array = byteBuffer.run { + val array = ByteArray(limit()) + position(0) + get(array) + rewind() + array + } + + val responseUnaryCallEvent = UnaryCallEvent( + uri = uri, + buffer = array + ) + + event.callbacks.forEach { callback -> + callback(responseUnaryCallEvent) + } + + if (responseUnaryCallEvent.canceled) { + param.setResult(null) + return@ephemeralHookObjectMethod + } + + methodParam.setArg(0, ByteBuffer.wrap(event.buffer)) + } + } + hookViewBinder() } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/UnaryCallEvent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/UnaryCallEvent.kt new file mode 100644 index 000000000..26245ed0e --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/UnaryCallEvent.kt @@ -0,0 +1,14 @@ +package me.rhunk.snapenhance.core.event.events.impl + +import me.rhunk.snapenhance.core.event.events.AbstractHookEvent + +class UnaryCallEvent( + val uri: String, + var buffer: ByteArray +): AbstractHookEvent() { + val callbacks = mutableListOf<(UnaryCallEvent) -> Unit>() + + fun addResponseCallback(responseCallback: UnaryCallEvent.() -> Unit) { + callbacks.add(responseCallback) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/DataClassBuilder.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/DataClassBuilder.kt new file mode 100644 index 000000000..ec174d2df --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/DataClassBuilder.kt @@ -0,0 +1,66 @@ +package me.rhunk.snapenhance.core.util + + +fun Any?.dataBuilder(dataClassBuilder: DataClassBuilder.() -> Unit): Any? { + return DataClassBuilder( + when (this) { + is Class<*> -> CallbackBuilder.createEmptyObject( + this.constructors.firstOrNull() ?: return null + ) ?: return null + else -> this + } ?: return null + ).apply(dataClassBuilder).build() +} + +// Util for building/editing data classes +class DataClassBuilder( + private val instance: Any, +) { + fun set(fieldName: String, value: Any?) { + val field = instance::class.java.declaredFields.firstOrNull { it.name == fieldName } ?: return + val fieldType = field.type + field.isAccessible = true + + when { + fieldType.isEnum -> { + val enumValue = fieldType.enumConstants.firstOrNull { it.toString() == value } ?: return + field.set(instance, enumValue) + } + fieldType.isPrimitive -> { + when (fieldType) { + Boolean::class.javaPrimitiveType -> field.setBoolean(instance, value as Boolean) + Byte::class.javaPrimitiveType -> field.setByte(instance, value as Byte) + Char::class.javaPrimitiveType -> field.setChar(instance, value as Char) + Short::class.javaPrimitiveType -> field.setShort(instance, value as Short) + Int::class.javaPrimitiveType -> field.setInt(instance, value as Int) + Long::class.javaPrimitiveType -> field.setLong(instance, value as Long) + Float::class.javaPrimitiveType -> field.setFloat(instance, value as Float) + Double::class.javaPrimitiveType -> field.setDouble(instance, value as Double) + } + } + else -> field.set(instance, value) + } + } + + fun set(vararg fields: Pair) = fields.forEach { set(it.first, it.second) } + + fun from(fieldName: String, new: Boolean = false, callback: DataClassBuilder.() -> Unit) { + val field = instance::class.java.declaredFields.firstOrNull { it.name == fieldName } ?: return + field.isAccessible = true + + val lazyInstance by lazy { CallbackBuilder.createEmptyObject(field.type.constructors.firstOrNull() ?: return@lazy null) ?: return@lazy null } + val builderInstance = if (new) lazyInstance else { + field.get(instance).takeIf { it != null } ?: lazyInstance + } + + DataClassBuilder(builderInstance ?: return).apply(callback) + + field.set(instance, builderInstance) + } + + fun cast(type: Class, callback: T.() -> Unit) { + type.cast(instance)?.let { callback(it) } + } + + fun build() = instance +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaPageViewControllerMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaPageViewControllerMapper.kt index dc28f03da..41025a4ac 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaPageViewControllerMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaPageViewControllerMapper.kt @@ -23,10 +23,10 @@ class OperaPageViewControllerMapper : AbstractClassMapper() { val layerListField = clazz.fields.first { it.type == "Ljava/util/ArrayList;" } - val onDisplayStateChange = clazz.methods.first { - if (it.returnType != "V" || it.parameterTypes.size != 1) return@first false - val firstParameterType = getClass(it.parameterTypes[0]) ?: return@first false - if (firstParameterType.type == clazz.type || !firstParameterType.isAbstract()) return@first false + val onDisplayStateChange = clazz.methods.firstOrNull { + if (it.returnType != "V" || it.parameterTypes.size != 1) return@firstOrNull false + val firstParameterType = getClass(it.parameterTypes[0]) ?: return@firstOrNull false + if (firstParameterType.type == clazz.type || !firstParameterType.isAbstract()) return@firstOrNull false //check if the class contains a field with the enumViewStateClass type firstParameterType.fields.any { field -> field.type == viewStateField.type @@ -44,7 +44,7 @@ class OperaPageViewControllerMapper : AbstractClassMapper() { "class" to clazz.getClassName(), "viewStateField" to viewStateField.name, "layerListField" to layerListField.name, - "onDisplayStateChange" to onDisplayStateChange.name, + "onDisplayStateChange" to onDisplayStateChange?.name, "onDisplayStateChangeGesture" to onDisplayStateChangeGesture.name ) From ed4334c429cc27633c1ad389ab8701eb50ab60b8 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 11 Jan 2024 23:10:09 +0100 Subject: [PATCH 45/53] refactor: mapper --- .../me/rhunk/snapenhance/RemoteSideContext.kt | 2 +- .../ui/manager/sections/home/HomeSection.kt | 2 +- .../common/bridge/wrapper/MappingsWrapper.kt | 96 ++------ .../me/rhunk/snapenhance/core/SnapEnhance.kt | 4 +- .../core/action/impl/BulkMessagingAction.kt | 30 +-- .../snapenhance/core/event/EventDispatcher.kt | 52 ++--- .../features/impl/ConfigurationOverride.kt | 214 +++++++++--------- .../impl/OperaViewerParamsOverride.kt | 42 ++-- .../impl/downloader/MediaDownloader.kt | 72 +++--- .../impl/experiments/AddFriendSourceSpoof.kt | 88 +++---- .../impl/experiments/InfiniteStoryBoost.kt | 16 +- .../impl/experiments/MeoPasscodeBypass.kt | 21 +- .../impl/experiments/NoFriendScoreDelay.kt | 12 +- .../global/BypassVideoLengthRestriction.kt | 17 +- .../impl/global/MediaQualityLevelOverride.kt | 20 +- .../core/features/impl/global/SnapchatPlus.kt | 23 +- .../core/features/impl/messaging/AutoSave.kt | 29 +-- .../core/features/impl/messaging/Messaging.kt | 67 +++--- .../features/impl/spying/HalfSwipeNotifier.kt | 6 +- .../core/features/impl/tweaks/CameraTweaks.kt | 25 +- .../features/impl/ui/HideFriendFeedEntry.kt | 40 ++-- .../impl/ui/HideQuickAddFriendFeed.kt | 14 +- .../core/features/impl/ui/SnapPreview.kt | 19 +- .../core/messaging/MessageSender.kt | 9 +- .../core/wrapper/impl/ConversationManager.kt | 28 ++- .../snapenhance/mapper/AbstractClassMapper.kt | 97 +++++++- .../mapper/{Mapper.kt => ClassMapper.kt} | 50 ++-- .../rhunk/snapenhance/mapper/MapperContext.kt | 38 ---- .../mapper/impl/BCryptClassMapper.kt | 13 +- .../snapenhance/mapper/impl/CallbackMapper.kt | 7 +- .../CompositeConfigurationProviderMapper.kt | 55 +++-- .../mapper/impl/DefaultMediaItemMapper.kt | 13 +- .../impl/FriendRelationshipChangerMapper.kt | 21 +- .../mapper/impl/FriendingDataSourcesMapper.kt | 13 +- .../impl/FriendsFeedEventDispatcherMapper.kt | 13 +- .../impl/MediaQualityLevelProviderMapper.kt | 12 +- .../impl/OperaPageViewControllerMapper.kt | 30 +-- .../mapper/impl/OperaViewerParamsMapper.kt | 14 +- .../mapper/impl/PlusSubscriptionMapper.kt | 18 +- .../mapper/impl/ScCameraSettingsMapper.kt | 6 +- .../mapper/impl/ScoreUpdateMapper.kt | 6 +- .../mapper/impl/StoryBoostStateMapper.kt | 6 +- .../mapper/impl/ViewBinderMapper.kt | 18 +- .../snapenhance/mapper/tests/TestMappings.kt | 29 +-- 44 files changed, 765 insertions(+), 642 deletions(-) rename mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/{Mapper.kt => ClassMapper.kt} (58%) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt index 1f3796319..a102fc77e 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -195,7 +195,7 @@ class RemoteSideContext( } } - if (mappings.isMappingsOutdated() || !mappings.isMappingsLoaded()) { + if (mappings.isMappingsOutdated() || !mappings.isMappingsLoaded) { requirements = requirements or Requirements.MAPPINGS } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt index 4c7dca85d..6c94895f4 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt @@ -110,7 +110,7 @@ class HomeSection : Section() { } override fun onResumed() { - if (!context.mappings.isMappingsLoaded()) { + if (!context.mappings.isMappingsLoaded) { context.mappings.init(context.androidContext) } context.coroutineScope.launch { diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt index 3f341925b..12880e30f 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt @@ -1,44 +1,24 @@ package me.rhunk.snapenhance.common.bridge.wrapper import android.content.Context -import com.google.gson.GsonBuilder -import com.google.gson.JsonElement import com.google.gson.JsonParser +import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.common.BuildConfig import me.rhunk.snapenhance.common.Constants import me.rhunk.snapenhance.common.bridge.FileLoaderWrapper import me.rhunk.snapenhance.common.bridge.types.BridgeFileType -import me.rhunk.snapenhance.mapper.Mapper -import me.rhunk.snapenhance.mapper.impl.* -import java.util.concurrent.ConcurrentHashMap -import kotlin.system.measureTimeMillis +import me.rhunk.snapenhance.common.logger.AbstractLogger +import me.rhunk.snapenhance.mapper.AbstractClassMapper +import me.rhunk.snapenhance.mapper.ClassMapper +import kotlin.reflect.KClass class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteArray(Charsets.UTF_8)) { - companion object { - private val gson = GsonBuilder().setPrettyPrinting().create() - private val mappers = arrayOf( - BCryptClassMapper::class, - CallbackMapper::class, - DefaultMediaItemMapper::class, - MediaQualityLevelProviderMapper::class, - OperaPageViewControllerMapper::class, - PlusSubscriptionMapper::class, - ScCameraSettingsMapper::class, - StoryBoostStateMapper::class, - FriendsFeedEventDispatcherMapper::class, - CompositeConfigurationProviderMapper::class, - ScoreUpdateMapper::class, - FriendRelationshipChangerMapper::class, - ViewBinderMapper::class, - FriendingDataSourcesMapper::class, - OperaViewerParamsMapper::class, - ) - } - private lateinit var context: Context - - private val mappings = ConcurrentHashMap() private var mappingUniqueHash: Long = 0 + var isMappingsLoaded = false + private set + + private val mappers = ClassMapper.DEFAULT_MAPPERS.associateBy { it::class } private fun getUniqueBuildId() = (getSnapchatPackageInfo()?.longVersionCode ?: -1) xor BuildConfig.BUILD_HASH.hashCode().toLong() @@ -63,8 +43,7 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr }.getOrNull() fun getGeneratedBuildNumber() = mappingUniqueHash - fun isMappingsOutdated() = mappingUniqueHash != getUniqueBuildId() || isMappingsLoaded().not() - fun isMappingsLoaded() = mappings.isNotEmpty() + fun isMappingsOutdated() = mappingUniqueHash != getUniqueBuildId() || isMappingsLoaded.not() private fun loadCached() { if (!isFileExists()) { @@ -74,64 +53,39 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr mappingUniqueHash = it["unique_hash"].asLong } - mappingsObject.entrySet().forEach { (key, value): Map.Entry -> - if (value.isJsonArray) { - mappings[key] = gson.fromJson(value, ArrayList::class.java) - return@forEach + mappingsObject.entrySet().forEach { (key, value) -> + mappers.values.firstOrNull { it.mapperName == key }?.let { mapper -> + mapper.readFromJson(value.asJsonObject) + mapper.classLoader = context.classLoader } - if (value.isJsonObject) { - mappings[key] = gson.fromJson(value, ConcurrentHashMap::class.java) - return@forEach - } - mappings[key] = value.asString } + isMappingsLoaded = true } fun refresh() { mappingUniqueHash = getUniqueBuildId() - val mapper = Mapper(*mappers) + val classMapper = ClassMapper(*mappers.values.toTypedArray()) runCatching { - mapper.loadApk(getSnapchatPackageInfo()?.applicationInfo?.sourceDir ?: throw Exception("Failed to get APK")) + classMapper.loadApk(getSnapchatPackageInfo()?.applicationInfo?.sourceDir ?: throw Exception("Failed to get APK")) }.onFailure { throw Exception("Failed to load APK", it) } - measureTimeMillis { - val result = mapper.start().apply { + runBlocking { + val result = classMapper.run().apply { addProperty("unique_hash", mappingUniqueHash) } write(result.toString().toByteArray()) } } - fun getMappedObject(key: String): Any { - if (mappings.containsKey(key)) { - return mappings[key]!! - } - throw Exception("No mapping found for $key") - } - - fun getMappedObjectNullable(key: String): Any? { - return mappings[key] - } - - fun getMappedClass(className: String): Class<*>? { - return runCatching { - context.classLoader.loadClass(getMappedObject(className) as? String) - }.getOrNull() - } - - fun getMappedClass(key: String, subKey: String): Class<*> { - return context.classLoader.loadClass(getMappedValue(key, subKey)) - } - - fun getMappedValue(key: String, subKey: String): String? { - return getMappedMap(key)?.get(subKey) as? String - } - @Suppress("UNCHECKED_CAST") - fun getMappedMap(key: String): Map? { - return getMappedObjectNullable(key) as? Map + fun useMapper(type: KClass, callback: T.() -> Unit) { + mappers[type]?.let { + callback(it as? T ?: return) + } ?: run { + AbstractLogger.directError("Mapper ${type.simpleName} is not registered", Throwable()) + } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt index 1a611c0dc..7a5e28d1c 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -91,7 +91,7 @@ class SnapEnhance { hookMainActivity("onCreate") { val isMainActivityNotNull = appContext.mainActivity != null appContext.mainActivity = this - if (isMainActivityNotNull || !appContext.mappings.isMappingsLoaded()) return@hookMainActivity + if (isMainActivityNotNull || !appContext.mappings.isMappingsLoaded) return@hookMainActivity onActivityCreate() jetpackComposeResourceHook() appContext.actionManager.onNewIntent(intent) @@ -146,7 +146,7 @@ class SnapEnhance { database.init() eventDispatcher.init() //if mappings aren't loaded, we can't initialize features - if (!mappings.isMappingsLoaded()) return + if (!mappings.isMappingsLoaded) return bridgeClient.registerMessagingBridge(messagingBridge) features.init() scriptRuntime.connect(bridgeClient.getScriptingInterface()) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt index 3ded9b814..a10f9f489 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt @@ -12,6 +12,7 @@ import me.rhunk.snapenhance.core.features.impl.experiments.AddFriendSourceSpoof import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.messaging.EnumBulkAction import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper +import me.rhunk.snapenhance.mapper.impl.FriendRelationshipChangerMapper class BulkMessagingAction : AbstractAction() { private val translation by lazy { context.translation.getCategory("bulk_messaging_action") } @@ -145,22 +146,23 @@ class BulkMessagingAction : AbstractAction() { } private fun removeFriend(userId: String) { - val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger") ?: throw Exception("Failed to get FriendRelationshipChanger mapping") - val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance!! + context.mappings.useMapper(FriendRelationshipChangerMapper::class) { + val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance!! + val removeFriendMethod = friendRelationshipChangerInstance::class.java.methods.first { + it.name == this.removeFriendMethod.get() + } - val removeFriendMethod = friendRelationshipChangerInstance::class.java.methods.first { - it.name == friendRelationshipChangerMapping["removeFriendMethod"].toString() + val completable = removeFriendMethod.invoke(friendRelationshipChangerInstance, + userId, // userId + removeFriendMethod.parameterTypes[1].enumConstants.first { it.toString() == "DELETED_BY_MY_FRIENDS" }, // source + null, // unknown + null, // unknown + null // InteractionPlacementInfo + )!! + completable::class.java.methods.first { + it.name == "subscribe" && it.parameterTypes.isEmpty() + }.invoke(completable) } - val completable = removeFriendMethod.invoke(friendRelationshipChangerInstance, - userId, // userId - removeFriendMethod.parameterTypes[1].enumConstants.first { it.toString() == "DELETED_BY_MY_FRIENDS" }, // source - null, // unknown - null, // unknown - null // InteractionPlacementInfo - )!! - completable::class.java.methods.first { - it.name == "subscribe" && it.parameterTypes.isEmpty() - }.invoke(completable) } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt index f0b2755fa..9ca3d96e1 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt @@ -18,44 +18,44 @@ import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.MessageContent import me.rhunk.snapenhance.core.wrapper.impl.MessageDestinations import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.mapper.impl.ViewBinderMapper import java.nio.ByteBuffer class EventDispatcher( private val context: ModContext ) : Manager { - private fun findClass(name: String) = context.androidContext.classLoader.loadClass(name) - private fun hookViewBinder() { - val cachedHooks = mutableListOf() - val viewBinderMappings = runCatching { context.mappings.getMappedMap("ViewBinder") }.getOrNull() ?: return - - fun cacheHook(clazz: Class<*>, block: Class<*>.() -> Unit) { - if (!cachedHooks.contains(clazz.name)) { - clazz.block() - cachedHooks.add(clazz.name) + context.mappings.useMapper(ViewBinderMapper::class) { + val cachedHooks = mutableListOf() + fun cacheHook(clazz: Class<*>, block: Class<*>.() -> Unit) { + if (!cachedHooks.contains(clazz.name)) { + clazz.block() + cachedHooks.add(clazz.name) + } } - } - findClass(viewBinderMappings["class"].toString()).hookConstructor(HookStage.AFTER) { methodParam -> - cacheHook( - methodParam.thisObject()::class.java - ) { - hook(viewBinderMappings["bindMethod"].toString(), HookStage.AFTER) bindViewMethod@{ param -> - val instance = param.thisObject() - val view = instance::class.java.methods.first { - it.name == viewBinderMappings["getViewMethod"].toString() - }.invoke(instance) as? View ?: return@bindViewMethod - - context.event.post( - BindViewEvent( - prevModel = param.arg(0), - nextModel = param.argNullable(1), - view = view + classReference.get()?.hookConstructor(HookStage.AFTER) { methodParam -> + cacheHook( + methodParam.thisObject()::class.java + ) { + hook(bindMethod.get().toString(), HookStage.AFTER) bindViewMethod@{ param -> + val instance = param.thisObject() + val view = instance::class.java.methods.firstOrNull { + it.name == getViewMethod.get().toString() + }?.invoke(instance) as? View ?: return@bindViewMethod + + context.event.post( + BindViewEvent( + prevModel = param.arg(0), + nextModel = param.argNullable(1), + view = view + ) ) - ) + } } } } + } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt index 1b14deec9..75ffe2f73 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt @@ -8,6 +8,7 @@ import me.rhunk.snapenhance.core.util.hook.Hooker import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField +import me.rhunk.snapenhance.mapper.impl.CompositeConfigurationProviderMapper data class ConfigKeyInfo( val category: String?, @@ -23,128 +24,129 @@ data class ConfigFilter( class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { - val compositeConfigurationProviderMappings = context.mappings.getMappedMap("CompositeConfigurationProvider") ?: throw Exception("Failed to get compositeConfigurationProviderMappings") - val enumMappings = compositeConfigurationProviderMappings["enum"] as Map<*, *> - - fun getConfigKeyInfo(key: Any?) = runCatching { - if (key == null) return@runCatching null - val keyClassMethods = key::class.java.methods - val keyName = keyClassMethods.firstOrNull { it.name == "getName" }?.invoke(key)?.toString() ?: key.toString() - val category = keyClassMethods.firstOrNull { it.name == enumMappings["getCategory"].toString() }?.invoke(key)?.toString() ?: return null - val valueHolder = keyClassMethods.firstOrNull { it.name == enumMappings["getValue"].toString() }?.invoke(key) ?: return null - val defaultValue = valueHolder.getObjectField(enumMappings["defaultValueField"].toString()) ?: return null - ConfigKeyInfo(category, keyName, defaultValue) - }.onFailure { - context.log.error("Failed to get config key info", it) - }.getOrNull() - - val propertyOverrides = mutableMapOf() - - fun overrideProperty(key: String, filter: (ConfigKeyInfo) -> Boolean, value: (ConfigKeyInfo) -> Any?, isAppExperiment: Boolean = false) { - propertyOverrides[key] = ConfigFilter(filter, value, isAppExperiment) - } - - overrideProperty("STREAK_EXPIRATION_INFO", { context.config.userInterface.streakExpirationInfo.get() }, - { true }) - overrideProperty("TRANSCODING_MAX_QUALITY", { context.config.global.forceUploadSourceQuality.get() }, - { true }, isAppExperiment = true) - - overrideProperty("CAMERA_ME_ENABLE_HEVC_RECORDING", { context.config.camera.hevcRecording.get() }, - { true }) - overrideProperty("MEDIA_RECORDER_MAX_QUALITY_LEVEL", { context.config.camera.forceCameraSourceEncoding.get() }, - { true }) - overrideProperty("REDUCE_MY_PROFILE_UI_COMPLEXITY", { context.config.userInterface.mapFriendNameTags.get() }, - { true }) - overrideProperty("ENABLE_LONG_SNAP_SENDING", { context.config.global.disableSnapSplitting.get() }, - { true }) - - overrideProperty("DF_VOPERA_FOR_STORIES", { context.config.userInterface.verticalStoryViewer.get() }, - { true }, isAppExperiment = true) - overrideProperty("SPOTLIGHT_5TH_TAB_ENABLED", { context.config.userInterface.disableSpotlight.get() }, - { false }) - - overrideProperty("BYPASS_AD_FEATURE_GATE", { context.config.global.blockAds.get() }, - { true }) - arrayOf("CUSTOM_AD_TRACKER_URL", "CUSTOM_AD_INIT_SERVER_URL", "CUSTOM_AD_SERVER_URL", "INIT_PRIMARY_URL", "INIT_SHADOW_URL").forEach { - overrideProperty(it, { context.config.global.blockAds.get() }, { "http://127.0.0.1" }) - } - - findClass(compositeConfigurationProviderMappings["class"].toString()).hook( - compositeConfigurationProviderMappings["getProperty"].toString(), - HookStage.AFTER - ) { param -> - val propertyKey = getConfigKeyInfo(param.argNullable(0)) ?: return@hook - - propertyOverrides[propertyKey.name]?.let { (filter, value) -> - if (!filter(propertyKey)) return@let - param.setResult(value(propertyKey)) + context.mappings.useMapper(CompositeConfigurationProviderMapper::class) { + fun getConfigKeyInfo(key: Any?) = runCatching { + if (key == null) return@runCatching null + val keyClassMethods = key::class.java.methods + val keyName = keyClassMethods.firstOrNull { it.name == "getName" }?.invoke(key)?.toString() ?: key.toString() + val category = keyClassMethods.firstOrNull { it.name == configEnumMapping["getCategory"]?.get().toString() }?.invoke(key)?.toString() ?: return null + val valueHolder = keyClassMethods.firstOrNull { it.name == configEnumMapping["getValue"]?.get().toString() }?.invoke(key) ?: return null + val defaultValue = valueHolder.getObjectField(configEnumMapping["defaultValueField"]?.get().toString()) ?: return null + ConfigKeyInfo(category, keyName, defaultValue) + }.onFailure { + context.log.error("Failed to get config key info", it) + }.getOrNull() + + val propertyOverrides = mutableMapOf() + + fun overrideProperty(key: String, filter: (ConfigKeyInfo) -> Boolean, value: (ConfigKeyInfo) -> Any?, isAppExperiment: Boolean = false) { + propertyOverrides[key] = ConfigFilter(filter, value, isAppExperiment) } - } - findClass(compositeConfigurationProviderMappings["class"].toString()).hook( - compositeConfigurationProviderMappings["observeProperty"].toString(), - HookStage.BEFORE - ) { param -> - val enumData = param.arg(0) - val key = enumData.toString() - val setValue: (Any?) -> Unit = { value -> - val valueHolder = XposedHelpers.callMethod(enumData, enumMappings["getValue"].toString()) - valueHolder.setObjectField(enumMappings["defaultValueField"].toString(), value) + overrideProperty("STREAK_EXPIRATION_INFO", { context.config.userInterface.streakExpirationInfo.get() }, + { true }) + overrideProperty("TRANSCODING_MAX_QUALITY", { context.config.global.forceUploadSourceQuality.get() }, + { true }, isAppExperiment = true) + + overrideProperty("CAMERA_ME_ENABLE_HEVC_RECORDING", { context.config.camera.hevcRecording.get() }, + { true }) + overrideProperty("MEDIA_RECORDER_MAX_QUALITY_LEVEL", { context.config.camera.forceCameraSourceEncoding.get() }, + { true }) + overrideProperty("REDUCE_MY_PROFILE_UI_COMPLEXITY", { context.config.userInterface.mapFriendNameTags.get() }, + { true }) + overrideProperty("ENABLE_LONG_SNAP_SENDING", { context.config.global.disableSnapSplitting.get() }, + { true }) + + overrideProperty("DF_VOPERA_FOR_STORIES", { context.config.userInterface.verticalStoryViewer.get() }, + { true }, isAppExperiment = true) + overrideProperty("SPOTLIGHT_5TH_TAB_ENABLED", { context.config.userInterface.disableSpotlight.get() }, + { false }) + + overrideProperty("BYPASS_AD_FEATURE_GATE", { context.config.global.blockAds.get() }, + { true }) + arrayOf("CUSTOM_AD_TRACKER_URL", "CUSTOM_AD_INIT_SERVER_URL", "CUSTOM_AD_SERVER_URL", "INIT_PRIMARY_URL", "INIT_SHADOW_URL").forEach { + overrideProperty(it, { context.config.global.blockAds.get() }, { "http://127.0.0.1" }) } - propertyOverrides[key]?.let { (filter, value) -> - val keyInfo = getConfigKeyInfo(enumData) ?: return@let - if (!filter(keyInfo)) return@let - setValue(value(keyInfo)) - } - } + classReference.getAsClass()?.hook( + getProperty.getAsString()!!, + HookStage.AFTER + ) { param -> + val propertyKey = getConfigKeyInfo(param.argNullable(0)) ?: return@hook - runCatching { - val appExperimentProviderMappings = compositeConfigurationProviderMappings["appExperimentProvider"] as Map<*, *> - val customBooleanPropertyRules = mutableListOf<(ConfigKeyInfo) -> Boolean>() + propertyOverrides[propertyKey.name]?.let { (filter, value) -> + if (!filter(propertyKey)) return@let + param.setResult(value(propertyKey)) + } + } - findClass(appExperimentProviderMappings["GetBooleanAppExperimentClass"].toString()).hook("invoke", HookStage.BEFORE) { param -> - val keyInfo = getConfigKeyInfo(param.arg(1)) ?: return@hook - if (customBooleanPropertyRules.any { it(keyInfo) }) { - param.setResult(true) - return@hook + classReference.get()?.hook( + observeProperty.getAsString()!!, + HookStage.BEFORE + ) { param -> + val enumData = param.arg(0) + val key = enumData.toString() + val setValue: (Any?) -> Unit = { value -> + val valueHolder = XposedHelpers.callMethod(enumData, configEnumMapping["getValue"]?.getAsString()) + valueHolder.setObjectField(configEnumMapping["defaultValueField"]?.getAsString()!!, value) } - propertyOverrides[keyInfo.name]?.let { (filter, value, isAppExperiment) -> - if (!isAppExperiment || !filter(keyInfo)) return@let - param.setResult(value(keyInfo)) + + propertyOverrides[key]?.let { (filter, value) -> + val keyInfo = getConfigKeyInfo(enumData) ?: return@let + if (!filter(keyInfo)) return@let + setValue(value(keyInfo)) } } - Hooker.ephemeralHookConstructor( - findClass(compositeConfigurationProviderMappings["class"].toString()), - HookStage.AFTER - ) { constructorParam -> - val instance = constructorParam.thisObject() - val appExperimentProviderInstance = instance::class.java.fields.firstOrNull { - findClass(appExperimentProviderMappings["class"].toString()).isAssignableFrom(it.type) - }?.get(instance) ?: return@ephemeralHookConstructor - - appExperimentProviderInstance::class.java.methods.first { - it.name == appExperimentProviderMappings["hasExperimentMethod"].toString() - }.hook(HookStage.BEFORE) { param -> - val keyInfo = getConfigKeyInfo(param.arg(0)) ?: return@hook - if (customBooleanPropertyRules.any { it(keyInfo) }) { - param.setResult(true) - return@hook + runCatching { + val customBooleanPropertyRules = mutableListOf<(ConfigKeyInfo) -> Boolean>() + + appExperimentProvider["getBooleanAppExperimentClass"]?.getAsClass() + ?.hook("invoke", HookStage.BEFORE) { param -> + val keyInfo = getConfigKeyInfo(param.arg(1)) ?: return@hook + if (customBooleanPropertyRules.any { it(keyInfo) }) { + param.setResult(true) + return@hook + } + propertyOverrides[keyInfo.name]?.let { (filter, value, isAppExperiment) -> + if (!isAppExperiment || !filter(keyInfo)) return@let + param.setResult(value(keyInfo)) + } } - val propertyOverride = propertyOverrides[keyInfo.name] ?: return@hook - if (propertyOverride.isAppExperiment && propertyOverride.filter(keyInfo)) param.setResult(true) + + + Hooker.ephemeralHookConstructor( + classReference.get()!!, + HookStage.AFTER + ) { constructorParam -> + val instance = constructorParam.thisObject() + val appExperimentProviderInstance = instance::class.java.fields.firstOrNull { + appExperimentProvider["class"]?.getAsClass()?.isAssignableFrom(it.type) == true + }?.get(instance) ?: return@ephemeralHookConstructor + + appExperimentProviderInstance::class.java.methods.first { + it.name == appExperimentProvider["hasExperimentMethod"]?.getAsString().toString() + }.hook(HookStage.BEFORE) { param -> + val keyInfo = getConfigKeyInfo(param.arg(0)) ?: return@hook + if (customBooleanPropertyRules.any { it(keyInfo) }) { + param.setResult(true) + return@hook + } + + val propertyOverride = propertyOverrides[keyInfo.name] ?: return@hook + if (propertyOverride.isAppExperiment && propertyOverride.filter(keyInfo)) param.setResult(true) + } } - } - if (context.config.experimental.hiddenSnapchatPlusFeatures.get()) { - customBooleanPropertyRules.add { key -> - key.category == "PLUS" && key.defaultValue is Boolean && key.name?.endsWith("_GATE") == true + if (context.config.experimental.hiddenSnapchatPlusFeatures.get()) { + customBooleanPropertyRules.add { key -> + key.category == "PLUS" && key.defaultValue is Boolean && key.name?.endsWith("_GATE") == true + } } + }.onFailure { + context.log.error("Failed to hook appExperimentProvider", it) } - }.onFailure { - context.log.error("Failed to hook appExperimentProvider", it) } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt index e6551a65f..638bd6e78 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt @@ -4,6 +4,7 @@ import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.mapper.impl.OperaViewerParamsMapper class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { data class OverrideKey( @@ -17,7 +18,6 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam ) override fun onActivityCreate() { - val operaViewerParamsMappings = context.mappings.getMappedMap("OperaViewerParams") ?: throw Exception("Failed to get operaViewerParamsMappings") val overrideMap = mutableMapOf() fun overrideParam(key: String, filter: (value: Any?) -> Boolean, value: (overrideKey: OverrideKey, value: Any?) -> Any?) { @@ -36,26 +36,28 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam }) } - findClass(operaViewerParamsMappings["class"].toString()).hook(operaViewerParamsMappings["putMethod"].toString(), HookStage.BEFORE) { param -> - val key = param.argNullable(0)?.let { key -> - val fields = key::class.java.fields - OverrideKey( - name = fields.firstOrNull { - it.type == String::class.java - }?.get(key)?.toString() ?: return@hook, - defaultValue = fields.firstOrNull { - it.type == Object::class.java - }?.get(key) - ) - } ?: return@hook - val value = param.argNullable(1) ?: return@hook + context.mappings.useMapper(OperaViewerParamsMapper::class) { + classReference.get()?.hook(putMethod.get()!!, HookStage.BEFORE) { param -> + val key = param.argNullable(0)?.let { key -> + val fields = key::class.java.fields + OverrideKey( + name = fields.firstOrNull { + it.type == String::class.java + }?.get(key)?.toString() ?: return@hook, + defaultValue = fields.firstOrNull { + it.type == Object::class.java + }?.get(key) + ) + } ?: return@hook + val value = param.argNullable(1) ?: return@hook - overrideMap[key.name]?.let { override -> - if (override.filter(value)) { - runCatching { - param.setArg(1, override.value(key, value)) - }.onFailure { - context.log.error("Failed to override param $key", it) + overrideMap[key.name]?.let { override -> + if (override.filter(value)) { + runCatching { + param.setArg(1, override.value(key, value)) + }.onFailure { + context.log.error("Failed to override param $key", it) + } } } } 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 index b398669b9..7b764dd67 100644 --- 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 @@ -47,6 +47,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.media.dash.SnapPlaylistItem import me.rhunk.snapenhance.core.wrapper.impl.media.opera.Layer import me.rhunk.snapenhance.core.wrapper.impl.media.opera.ParamMap import me.rhunk.snapenhance.core.wrapper.impl.media.toKeyPair +import me.rhunk.snapenhance.mapper.impl.OperaPageViewControllerMapper import java.io.ByteArrayInputStream import java.nio.file.Paths import java.util.UUID @@ -462,51 +463,50 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } override fun asyncOnActivityCreate() { - val operaViewerControllerClass: Class<*> = context.mappings.getMappedClass("OperaPageViewController", "class") - - val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param -> - - val viewState = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "viewStateField")!!).toString() - if (viewState != "FULLY_DISPLAYED") { - return@onOperaViewStateCallback - } - val operaLayerList = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "layerListField")!!) as ArrayList<*> - val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap + context.mappings.useMapper(OperaPageViewControllerMapper::class) { + val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param -> + val viewState = (param.thisObject() as Any).getObjectField(viewStateField.get()!!).toString() + if (viewState != "FULLY_DISPLAYED") { + return@onOperaViewStateCallback + } + val operaLayerList = (param.thisObject() as Any).getObjectField(layerListField.get()!!) as ArrayList<*> + val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap - if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list")) - return@onOperaViewStateCallback + if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list")) + return@onOperaViewStateCallback - val mediaInfoMap = mutableMapOf() - val isVideo = mediaParamMap.containsKey("video_media_info_list") + val mediaInfoMap = mutableMapOf() + val isVideo = mediaParamMap.containsKey("video_media_info_list") - mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo( - (if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!! - ) + mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo( + (if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!! + ) - if (context.config.downloader.mergeOverlays.get() && mediaParamMap.containsKey("overlay_image_media_info")) { - mediaInfoMap[SplitMediaAssetType.OVERLAY] = - MediaInfo(mediaParamMap["overlay_image_media_info"]!!) - } - lastSeenMapParams = mediaParamMap - lastSeenMediaInfoMap = mediaInfoMap + if (context.config.downloader.mergeOverlays.get() && mediaParamMap.containsKey("overlay_image_media_info")) { + mediaInfoMap[SplitMediaAssetType.OVERLAY] = + MediaInfo(mediaParamMap["overlay_image_media_info"]!!) + } + lastSeenMapParams = mediaParamMap + lastSeenMediaInfoMap = mediaInfoMap - if (!canAutoDownload()) return@onOperaViewStateCallback + if (!canAutoDownload()) return@onOperaViewStateCallback - context.executeAsync { - runCatching { - handleOperaMedia(mediaParamMap, mediaInfoMap, false) - }.onFailure { - context.log.error("Failed to handle opera media", it) - context.longToast(it.message) + context.executeAsync { + runCatching { + handleOperaMedia(mediaParamMap, mediaInfoMap, false) + }.onFailure { + context.log.error("Failed to handle opera media", it) + context.longToast(it.message) + } } } - } - arrayOf("onDisplayStateChange", "onDisplayStateChangeGesture").forEach { methodName -> - operaViewerControllerClass.hook( - context.mappings.getMappedValue("OperaPageViewController", methodName) ?: return@forEach, - HookStage.AFTER, onOperaViewStateCallback - ) + arrayOf(onDisplayStateChange, onDisplayStateChangeGesture).forEach { methodName -> + classReference.get()?.hook( + methodName.get() ?: return@forEach, + HookStage.AFTER, onOperaViewStateCallback + ) + } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt index 83b5e4475..32873b37c 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt @@ -5,61 +5,61 @@ import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hookConstructor +import me.rhunk.snapenhance.mapper.impl.FriendRelationshipChangerMapper class AddFriendSourceSpoof : Feature("AddFriendSourceSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { var friendRelationshipChangerInstance: Any? = null private set override fun onActivityCreate() { - val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger") ?: throw Exception("Failed to get friendRelationshipChangerMapping") - - findClass(friendRelationshipChangerMapping["class"].toString()).hookConstructor(HookStage.AFTER) { param -> - friendRelationshipChangerInstance = param.thisObject() - } + context.mappings.useMapper(FriendRelationshipChangerMapper::class) { + classReference.get()?.hookConstructor(HookStage.AFTER) { param -> + friendRelationshipChangerInstance = param.thisObject() + } - findClass(friendRelationshipChangerMapping["class"].toString()) - .hook(friendRelationshipChangerMapping["addFriendMethod"].toString(), HookStage.BEFORE) { param -> - val spoofedSource = context.config.experimental.addFriendSourceSpoof.getNullable() ?: return@hook + classReference.get()?.hook(addFriendMethod.get()!!, HookStage.BEFORE) { param -> + val spoofedSource = context.config.experimental.addFriendSourceSpoof.getNullable() ?: return@hook - fun setEnum(index: Int, value: String) { - val enumData = param.arg(index) - enumData::class.java.enumConstants.first { it.toString() == value }.let { - param.setArg(index, it) + fun setEnum(index: Int, value: String) { + val enumData = param.arg(index) + enumData::class.java.enumConstants.first { it.toString() == value }.let { + param.setArg(index, it) + } } - } - when (spoofedSource) { - "added_by_quick_add" -> { - setEnum(1, "PROFILE") - setEnum(2, "ADD_FRIENDS_BUTTON_ON_TOP_BAR_ON_FRIENDS_FEED") - setEnum(3, "ADDED_BY_SUGGESTED") - } - "added_by_group_chat" -> { - setEnum(1, "PROFILE") - setEnum(2, "GROUP_PROFILE") - setEnum(3, "ADDED_BY_GROUP_CHAT") - } - "added_by_username" -> { - setEnum(1, "SEARCH") - setEnum(2, "SEARCH") - setEnum(3, "ADDED_BY_USERNAME") - } - "added_by_qr_code" -> { - setEnum(1, "PROFILE") - setEnum(2, "PROFILE") - setEnum(3, "ADDED_BY_QR_CODE") - } - "added_by_mention" -> { - setEnum(1, "CONTEXT_CARDS") - setEnum(2, "CONTEXT_CARD") - setEnum(3, "ADDED_BY_MENTION") - } - "added_by_community" -> { - setEnum(1, "PROFILE") - setEnum(2, "PROFILE") - setEnum(3, "ADDED_BY_COMMUNITY") + when (spoofedSource) { + "added_by_quick_add" -> { + setEnum(1, "PROFILE") + setEnum(2, "ADD_FRIENDS_BUTTON_ON_TOP_BAR_ON_FRIENDS_FEED") + setEnum(3, "ADDED_BY_SUGGESTED") + } + "added_by_group_chat" -> { + setEnum(1, "PROFILE") + setEnum(2, "GROUP_PROFILE") + setEnum(3, "ADDED_BY_GROUP_CHAT") + } + "added_by_username" -> { + setEnum(1, "SEARCH") + setEnum(2, "SEARCH") + setEnum(3, "ADDED_BY_USERNAME") + } + "added_by_qr_code" -> { + setEnum(1, "PROFILE") + setEnum(2, "PROFILE") + setEnum(3, "ADDED_BY_QR_CODE") + } + "added_by_mention" -> { + setEnum(1, "CONTEXT_CARDS") + setEnum(2, "CONTEXT_CARD") + setEnum(3, "ADDED_BY_MENTION") + } + "added_by_community" -> { + setEnum(1, "PROFILE") + setEnum(2, "PROFILE") + setEnum(3, "ADDED_BY_COMMUNITY") + } + else -> return@hook } - else -> return@hook } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt index 60b7455b5..7d4f29bf5 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt @@ -4,18 +4,20 @@ import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hookConstructor +import me.rhunk.snapenhance.mapper.impl.StoryBoostStateMapper class InfiniteStoryBoost : Feature("InfiniteStoryBoost", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { if (!context.config.experimental.infiniteStoryBoost.get()) return - val storyBoostStateClass = context.mappings.getMappedClass("StoryBoostStateClass") ?: throw Exception("Failed to get storyBoostStateClass") - storyBoostStateClass.hookConstructor(HookStage.BEFORE) { param -> - val startTimeMillis = param.arg(1) - //reset timestamp if it's more than 24 hours - if (System.currentTimeMillis() - startTimeMillis > 86400000) { - param.setArg(1, 0) - param.setArg(2, 0) + context.mappings.useMapper(StoryBoostStateMapper::class) { + classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> + val startTimeMillis = param.arg(1) + //reset timestamp if it's more than 24 hours + if (System.currentTimeMillis() - startTimeMillis > 86400000) { + param.setArg(1, 0) + param.setArg(2, 0) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt index 1b6157ae2..d53b446a0 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt @@ -3,20 +3,21 @@ package me.rhunk.snapenhance.core.features.impl.experiments import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage -import me.rhunk.snapenhance.core.util.hook.Hooker +import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.mapper.impl.BCryptClassMapper class MeoPasscodeBypass : Feature("Meo Passcode Bypass", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { - val bcrypt = context.mappings.getMappedMap("BCrypt") ?: throw Exception("Failed to get bcrypt mappings") + if (!context.config.experimental.meoPasscodeBypass.get()) return - Hooker.hook( - context.androidContext.classLoader.loadClass(bcrypt["class"].toString()), - bcrypt["hashMethod"].toString(), - HookStage.BEFORE, - { context.config.experimental.meoPasscodeBypass.get() }, - ) { param -> - //set the hash to the result of the method - param.setResult(param.arg(1)) + context.mappings.useMapper(BCryptClassMapper::class) { + classReference.get()?.hook( + hashMethod.get()!!, + HookStage.BEFORE, + ) { param -> + //set the hash to the result of the method + param.setResult(param.arg(1)) + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt index c69d3a8c9..35dd38c95 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt @@ -4,17 +4,19 @@ import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hookConstructor +import me.rhunk.snapenhance.mapper.impl.ScoreUpdateMapper import java.lang.reflect.Constructor class NoFriendScoreDelay : Feature("NoFriendScoreDelay", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { override fun onActivityCreate() { if (!context.config.experimental.noFriendScoreDelay.get()) return - val scoreUpdateClass = context.mappings.getMappedClass("ScoreUpdate") ?: throw Exception("Failed to get scoreUpdateClass") - scoreUpdateClass.hookConstructor(HookStage.BEFORE) { param -> - val constructor = param.method() as Constructor<*> - if (constructor.parameterTypes.size < 3 || constructor.parameterTypes[3] != java.util.Collection::class.java) return@hookConstructor - param.setArg(2, 0L) + context.mappings.useMapper(ScoreUpdateMapper::class) { + classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> + val constructor = param.method() as Constructor<*> + if (constructor.parameterTypes.size < 3 || constructor.parameterTypes[3] != java.util.Collection::class.java) return@hookConstructor + param.setArg(2, 0L) + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt index cbd3dae4b..d0ec43fdc 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt @@ -9,6 +9,7 @@ import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.ktx.setObjectField +import me.rhunk.snapenhance.mapper.impl.DefaultMediaItemMapper import java.io.File class BypassVideoLengthRestriction : @@ -45,21 +46,23 @@ class BypassVideoLengthRestriction : } } - context.mappings.getMappedClass("DefaultMediaItem") - ?.hookConstructor(HookStage.BEFORE) { param -> + context.mappings.useMapper(DefaultMediaItemMapper::class) { + defaultMediaItem.getAsClass()?.hookConstructor(HookStage.BEFORE) { param -> //set the video length argument param.setArg(5, -1L) } + } } //TODO: allow split from any source if (mode == "split") { - val cameraRollId = context.mappings.getMappedMap("CameraRollMediaId") ?: throw Exception("Failed to get cameraRollId mappings") // memories grid - findClass(cameraRollId["class"].toString()).hookConstructor(HookStage.AFTER) { param -> - //set the durationMs field - param.thisObject() - .setObjectField(cameraRollId["durationMsField"].toString(), -1L) + context.mappings.useMapper(DefaultMediaItemMapper::class) { + cameraRollMediaId.getAsClass()?.hookConstructor(HookStage.AFTER) { param -> + //set the durationMs field + param.thisObject() + .setObjectField(durationMsField.get()!!, -1L) + } } // chat camera roll grid diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaQualityLevelOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaQualityLevelOverride.kt index 0c8607200..1def2b676 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaQualityLevelOverride.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaQualityLevelOverride.kt @@ -4,20 +4,20 @@ import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.mapper.impl.MediaQualityLevelProviderMapper +import java.lang.reflect.Method class MediaQualityLevelOverride : Feature("MediaQualityLevelOverride", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { - val enumQualityLevel = context.mappings.getMappedClass("EnumQualityLevel") ?: throw Exception("Failed to get enumQualityLevelMappings") - val mediaQualityLevelProvider = context.mappings.getMappedMap("MediaQualityLevelProvider") ?: throw Exception("Failed to get mediaQualityLevelProviderMappings") + if (!context.config.global.forceUploadSourceQuality.get()) return - val forceMediaSourceQuality by context.config.global.forceUploadSourceQuality - - context.androidContext.classLoader.loadClass(mediaQualityLevelProvider["class"].toString()).hook( - mediaQualityLevelProvider["method"].toString(), - HookStage.BEFORE, - { forceMediaSourceQuality } - ) { param -> - param.setResult(enumQualityLevel.enumConstants.firstOrNull { it.toString() == "LEVEL_MAX" } ) + context.mappings.useMapper(MediaQualityLevelProviderMapper::class) { + mediaQualityLevelProvider.getAsClass()?.hook( + mediaQualityLevelProviderMethod.getAsString()!!, + HookStage.BEFORE + ) { param -> + param.setResult((param.method() as Method).returnType.enumConstants.firstOrNull { it.toString() == "LEVEL_MAX" } ) + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt index e553989a8..ecec5df5d 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt @@ -3,8 +3,9 @@ package me.rhunk.snapenhance.core.features.impl.global import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage -import me.rhunk.snapenhance.core.util.hook.Hooker import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.core.util.hook.hookConstructor +import me.rhunk.snapenhance.mapper.impl.PlusSubscriptionMapper class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.INIT_SYNC) { private val originalSubscriptionTime = (System.currentTimeMillis() - 7776000000L) @@ -13,17 +14,17 @@ class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.INIT_ override fun init() { if (!context.config.global.snapchatPlus.get()) return - val subscriptionInfoClass = context.mappings.getMappedClass("SubscriptionInfoClass") ?: throw Exception("Failed to get subscriptionInfoClass") + context.mappings.useMapper(PlusSubscriptionMapper::class) { + classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> + if (param.arg(0) == 2) return@hookConstructor + //subscription tier + param.setArg(0, 2) + //subscription status + param.setArg(1, 2) - Hooker.hookConstructor(subscriptionInfoClass, HookStage.BEFORE) { param -> - if (param.arg(0) == 2) return@hookConstructor - //subscription tier - param.setArg(0, 2) - //subscription status - param.setArg(1, 2) - - param.setArg(2, originalSubscriptionTime) - param.setArg(3, expirationTimeMillis) + param.setArg(2, originalSubscriptionTime) + param.setArg(3, expirationTimeMillis) + } } // optional as ConfigurationOverride does this too diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt index 00245c1ea..7ac764ea5 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt @@ -13,6 +13,7 @@ import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.mapper.impl.CallbackMapper import java.util.concurrent.Executors class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { @@ -70,19 +71,21 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, override fun asyncOnActivityCreate() { // called when enter in a conversation - context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback").hook( - "onFetchConversationWithMessagesComplete", - HookStage.BEFORE, - { autoSaveFilter.isNotEmpty() } - ) { param -> - val conversationId = SnapUUID(param.arg(0).getObjectField("mConversationId")!!) - if (!canSaveInConversation(conversationId.toString())) return@hook - - val messages = param.arg>(1).map { Message(it) } - messages.forEach { - if (!canSaveMessage(it)) return@forEach - asyncSaveExecutorService.submit { - saveMessage(conversationId.toString(), it) + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("FetchConversationWithMessagesCallback")?.hook( + "onFetchConversationWithMessagesComplete", + HookStage.BEFORE, + { autoSaveFilter.isNotEmpty() } + ) { param -> + val conversationId = SnapUUID(param.arg(0).getObjectField("mConversationId")!!) + if (!canSaveInConversation(conversationId.toString())) return@hook + + val messages = param.arg>(1).map { Message(it) } + messages.forEach { + if (!canSaveMessage(it)) return@forEach + asyncSaveExecutorService.submit { + saveMessage(conversationId.toString(), it) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt index 19bee0215..81c3f7006 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt @@ -18,6 +18,8 @@ import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import me.rhunk.snapenhance.core.wrapper.impl.Snapchatter import me.rhunk.snapenhance.core.wrapper.impl.toSnapUUID +import me.rhunk.snapenhance.mapper.impl.CallbackMapper +import me.rhunk.snapenhance.mapper.impl.FriendsFeedEventDispatcherMapper import java.util.concurrent.Future class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) { @@ -47,24 +49,28 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } } - context.mappings.getMappedClass("callbacks", "ConversationManagerDelegate").apply { - hookConstructor(HookStage.AFTER) { param -> - conversationManagerDelegate = param.thisObject() - } - hook("onConversationUpdated", HookStage.BEFORE) { param -> - context.event.post(ConversationUpdateEvent( - conversationId = SnapUUID(param.arg(0)).toString(), - conversation = param.argNullable(1), - messages = param.arg>(2).map { Message(it) }, - ).apply { adapter = param }) { - param.setArg(2, messages.map { it.instanceNonNull() }.toCollection(ArrayList())) + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("ConversationManagerDelegate")?.apply { + hookConstructor(HookStage.AFTER) { param -> + conversationManagerDelegate = param.thisObject() + } + hook("onConversationUpdated", HookStage.BEFORE) { param -> + context.event.post(ConversationUpdateEvent( + conversationId = SnapUUID(param.arg(0)).toString(), + conversation = param.argNullable(1), + messages = param.arg>(2).map { Message(it) }, + ).apply { adapter = param }) { + param.setArg( + 2, + messages.map { it.instanceNonNull() }.toCollection(ArrayList()) + ) + } } } - } - - context.mappings.getMappedClass("callbacks", "IdentityDelegate").apply { - hookConstructor(HookStage.AFTER) { - identityDelegate = it.thisObject() + callbacks.getClass("IdentityDelegate")?.apply { + hookConstructor(HookStage.AFTER) { + identityDelegate = it.thisObject() + } } } } @@ -96,10 +102,10 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } override fun onActivityCreate() { - context.mappings.getMappedObjectNullable("FriendsFeedEventDispatcher").let { it as? Map<*, *> }?.let { mappings -> - findClass(mappings["class"].toString()).hook("onItemLongPress", HookStage.BEFORE) { param -> + context.mappings.useMapper(FriendsFeedEventDispatcherMapper::class) { + classReference.getAsClass()?.hook("onItemLongPress", HookStage.BEFORE) { param -> val viewItemContainer = param.arg(0) - val viewItem = viewItemContainer.getObjectField(mappings["viewModelField"].toString()).toString() + val viewItem = viewItemContainer.getObjectField(viewModelField.get()!!).toString() val conversationId = viewItem.substringAfter("conversationId: ").substring(0, 36).also { if (it.startsWith("null")) return@hook } @@ -109,7 +115,6 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } } - context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param -> val instance = param.thisObject() val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor @@ -122,17 +127,21 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C }.sortedBy { it.orderKey }.mapNotNull { it.messageDescriptor?.messageId } } - context.mappings.getMappedClass("callbacks", "GetOneOnOneConversationIdsCallback").hook("onSuccess", HookStage.BEFORE) { param -> - val userIdToConversation = (param.arg>(0)) - .takeIf { it.isNotEmpty() } - ?.get(0) ?: return@hook + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("GetOneOnOneConversationIdsCallback")?.hook("onSuccess", HookStage.BEFORE) { param -> + val userIdToConversation = (param.arg>(0)) + .takeIf { it.isNotEmpty() } + ?.get(0) ?: return@hook - lastFetchConversationUUID = SnapUUID(userIdToConversation.getObjectField("mConversationId")) - lastFetchConversationUserUUID = SnapUUID(userIdToConversation.getObjectField("mUserId")) + lastFetchConversationUUID = + SnapUUID(userIdToConversation.getObjectField("mConversationId")) + lastFetchConversationUserUUID = + SnapUUID(userIdToConversation.getObjectField("mUserId")) + } } - with(context.classCache.conversationManager) { - Hooker.hook(this, "enterConversation", HookStage.BEFORE) { param -> + context.classCache.conversationManager.apply { + hook("enterConversation", HookStage.BEFORE) { param -> openedConversationUUID = SnapUUID(param.arg(0)) if (context.config.messaging.bypassMessageRetentionPolicy.get()) { val callback = param.argNullable(2) ?: return@hook @@ -141,7 +150,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } } - Hooker.hook(this, "exitConversation", HookStage.BEFORE) { + hook("exitConversation", HookStage.BEFORE) { openedConversationUUID = null } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt index 8f23d07f9..0e99250ec 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt @@ -12,6 +12,7 @@ import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.ktx.getIdentifier import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.mapper.impl.CallbackMapper import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration.Companion.milliseconds @@ -43,8 +44,8 @@ class HalfSwipeNotifier : Feature("Half Swipe Notifier", loadParams = FeatureLoa presenceService = it.thisObject() } - context.mappings.getMappedClass("callbacks", "PresenceServiceDelegate") - .hook("notifyActiveConversationsChanged", HookStage.BEFORE) { + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("PresenceServiceDelegate")?.hook("notifyActiveConversationsChanged", HookStage.BEFORE) { val activeConversations = presenceService::class.java.methods.find { it.name == "getActiveConversations" }?.invoke(presenceService) as? Map<*, *> ?: return@hook // conversationId, conversationInfo (this.mPeekingParticipants) if (activeConversations.isEmpty()) { @@ -75,6 +76,7 @@ class HalfSwipeNotifier : Feature("Half Swipe Notifier", loadParams = FeatureLoa } peekingConversations[conversationId.toString()] = peekingParticipantsIds } + } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt index 9ec25a9b5..ed9ca14c9 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt @@ -17,6 +17,7 @@ import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.wrapper.impl.ScSize +import me.rhunk.snapenhance.mapper.impl.ScCameraSettingsMapper import java.io.ByteArrayOutputStream import java.nio.ByteBuffer @@ -62,19 +63,21 @@ class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACT } } - context.mappings.getMappedClass("ScCameraSettings")?.hookConstructor(HookStage.BEFORE) { param -> - val previewResolution = ScSize(param.argNullable(2)) - val captureResolution = ScSize(param.argNullable(3)) + context.mappings.useMapper(ScCameraSettingsMapper::class) { + classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> + val previewResolution = ScSize(param.argNullable(2)) + val captureResolution = ScSize(param.argNullable(3)) - if (previewResolution.isPresent() && captureResolution.isPresent()) { - previewResolutionConfig?.let { - previewResolution.first = it[0] - previewResolution.second = it[1] - } + if (previewResolution.isPresent() && captureResolution.isPresent()) { + previewResolutionConfig?.let { + previewResolution.first = it[0] + previewResolution.second = it[1] + } - captureResolutionConfig?.let { - captureResolution.first = it[0] - captureResolution.second = it[1] + captureResolutionConfig?.let { + captureResolution.first = it[0] + captureResolution.second = it[1] + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideFriendFeedEntry.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideFriendFeedEntry.kt index edd78fb96..9b4dcfa43 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideFriendFeedEntry.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideFriendFeedEntry.kt @@ -9,6 +9,7 @@ import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.mapper.impl.CallbackMapper class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType = MessagingRuleType.HIDE_FRIEND_FEED, loadParams = FeatureLoadParams.INIT_SYNC) { private fun createDeletedFeedEntry(conversationId: String) = context.gson.fromJson( @@ -41,30 +42,31 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType override fun init() { if (!context.config.userInterface.hideFriendFeedEntry.get()) return - arrayOf( - "QueryFeedCallback" to "onQueryFeedComplete", - "FeedManagerDelegate" to "onFeedEntriesUpdated", - "FeedManagerDelegate" to "onInternalSyncFeed", - "SyncFeedCallback" to "onSyncFeedComplete", - ).forEach { (callbackName, methodName) -> - context.mappings.getMappedClass("callbacks", callbackName) - .hook(methodName, HookStage.BEFORE) { param -> + context.mappings.useMapper(CallbackMapper::class) { + arrayOf( + "QueryFeedCallback" to "onQueryFeedComplete", + "FeedManagerDelegate" to "onFeedEntriesUpdated", + "FeedManagerDelegate" to "onInternalSyncFeed", + "SyncFeedCallback" to "onSyncFeedComplete", + ).forEach { (callbackName, methodName) -> + findClass(callbacks.get()!![callbackName] ?: return@forEach).hook(methodName, HookStage.BEFORE) { param -> filterFriendFeed(param.arg(0)) } - } + } - context.mappings.getMappedClass("callbacks", "FetchAndSyncFeedCallback") - .hook("onFetchAndSyncFeedComplete", HookStage.BEFORE) { param -> - val deletedConversations: ArrayList = param.arg(2) - filterFriendFeed(param.arg(0), deletedConversations) + callbacks.getClass("FetchAndSyncFeedCallback") + ?.hook("onFetchAndSyncFeedComplete", HookStage.BEFORE) { param -> + val deletedConversations: ArrayList = param.arg(2) + filterFriendFeed(param.arg(0), deletedConversations) - if (deletedConversations.any { - val uuid = SnapUUID(it.getObjectField("mFeedEntryIdentifier")?.getObjectField("mConversationId")).toString() - context.database.getFeedEntryByConversationId(uuid) != null - }) { - param.setArg(4, true) + if (deletedConversations.any { + val uuid = SnapUUID(it.getObjectField("mFeedEntryIdentifier")?.getObjectField("mConversationId")).toString() + context.database.getFeedEntryByConversationId(uuid) != null + }) { + param.setArg(4, true) + } } - } + } } override fun getRuleState() = RuleState.WHITELIST diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt index 2f2b73042..7e3f97574 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt @@ -5,17 +5,19 @@ import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.ktx.setObjectField +import me.rhunk.snapenhance.mapper.impl.FriendingDataSourcesMapper class HideQuickAddFriendFeed : Feature("HideQuickAddFriendFeed", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { override fun onActivityCreate() { if (!context.config.userInterface.hideQuickAddFriendFeed.get()) return - val friendingDataSource = context.mappings.getMappedMap("FriendingDataSources") ?: throw Exception("Failed to get friendingDataSourceMappings") - findClass(friendingDataSource["class"].toString()).hookConstructor(HookStage.AFTER) { param -> - param.thisObject().setObjectField( - friendingDataSource["quickAddSourceListField"].toString(), - arrayListOf() - ) + context.mappings.useMapper(FriendingDataSourcesMapper::class) { + classReference.getAsClass()?.hookConstructor(HookStage.AFTER) { param -> + param.thisObject().setObjectField( + quickAddSourceListField.get()!!, + arrayListOf() + ) + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SnapPreview.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SnapPreview.kt index 86feee795..a64586120 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SnapPreview.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SnapPreview.kt @@ -19,6 +19,7 @@ import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getDimens import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.media.PreviewUtils +import me.rhunk.snapenhance.mapper.impl.CallbackMapper import java.io.File class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC) { @@ -29,17 +30,19 @@ class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_S override fun init() { if (!isEnabled) return - context.mappings.getMappedClass("callbacks", "ContentCallback").hook("handleContentResult", HookStage.BEFORE) { param -> - val contentResult = param.arg(0) - val classMethods = contentResult::class.java.methods + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("ContentCallback")?.hook("handleContentResult", HookStage.BEFORE) { param -> + val contentResult = param.arg(0) + val classMethods = contentResult::class.java.methods - val contentKey = classMethods.find { it.name == "getContentKey" }?.invoke(contentResult) ?: return@hook - if (contentKey.getObjectField("mMediaContextType").toString() != "CHAT") return@hook + val contentKey = classMethods.find { it.name == "getContentKey" }?.invoke(contentResult) ?: return@hook + if (contentKey.getObjectField("mMediaContextType").toString() != "CHAT") return@hook - val filePath = classMethods.find { it.name == "getFilePath" }?.invoke(contentResult) ?: return@hook - val mediaId = contentKey.getObjectField("mMediaId").toString() + val filePath = classMethods.find { it.name == "getFilePath" }?.invoke(contentResult) ?: return@hook + val mediaId = contentKey.getObjectField("mMediaId").toString() - mediaFileCache[mediaId.substringAfter("-")] = File(filePath.toString()) + mediaFileCache[mediaId.substringAfter("-")] = File(filePath.toString()) + } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessageSender.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessageSender.kt index 65e2698a4..d9687eb44 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessageSender.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessageSender.kt @@ -8,6 +8,7 @@ import me.rhunk.snapenhance.core.util.CallbackBuilder import me.rhunk.snapenhance.core.wrapper.AbstractWrapper import me.rhunk.snapenhance.core.wrapper.impl.MessageDestinations import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.mapper.impl.CallbackMapper class MessageSender( private val context: ModContext, @@ -55,7 +56,13 @@ class MessageSender( } - private val sendMessageCallback by lazy { context.mappings.getMappedClass("callbacks", "SendMessageCallback") } + private val sendMessageCallback by lazy { + lateinit var result: Class<*> + context.mappings.useMapper(CallbackMapper::class) { + result = callbacks.getClass("SendMessageCallback") ?: return@useMapper + } + result + } private fun createLocalMessageContentTemplate( contentType: ContentType, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt index 2f6fa863e..058a14adf 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt @@ -6,6 +6,7 @@ import me.rhunk.snapenhance.core.util.CallbackBuilder import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.wrapper.AbstractWrapper +import me.rhunk.snapenhance.mapper.impl.CallbackMapper typealias CallbackResult = (error: String?) -> Unit @@ -26,20 +27,29 @@ class ConversationManager( private val getOneOnOneConversationIds by lazy { findMethodByName("getOneOnOneConversationIds") } + private fun getCallbackClass(name: String): Class<*> { + lateinit var result: Class<*> + context.mappings.useMapper(CallbackMapper::class) { + result = context.androidContext.classLoader.loadClass(callbacks.get()!![name]) + } + return result + } + + fun updateMessage(conversationId: String, messageId: Long, action: MessageUpdate, onResult: CallbackResult = {}) { updateMessageMethod.invoke( instanceNonNull(), SnapUUID.fromString(conversationId).instanceNonNull(), messageId, context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == action.toString() }, - CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) + CallbackBuilder(getCallbackClass("Callback")) .override("onSuccess") { onResult(null) } .override("onError") { onResult(it.arg(0).toString()) }.build() ) } fun fetchConversationWithMessagesPaginated(conversationId: String, lastMessageId: Long, amount: Int, onSuccess: (message: List) -> Unit, onError: (error: String) -> Unit) { - val callback = CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback")) + val callback = CallbackBuilder(getCallbackClass("FetchConversationWithMessagesCallback")) .override("onFetchConversationWithMessagesComplete") { param -> onSuccess(param.arg>(1).map { Message(it) }) } @@ -54,7 +64,7 @@ class ConversationManager( fetchConversationWithMessagesMethod.invoke( instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(), - CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback")) + CallbackBuilder(getCallbackClass("FetchConversationWithMessagesCallback")) .override("onFetchConversationWithMessagesComplete") { param -> onSuccess(param.arg>(1).map { Message(it) }) } @@ -70,7 +80,7 @@ class ConversationManager( instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(), messageId, - CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) + CallbackBuilder(getCallbackClass("Callback")) .override("onSuccess") { onResult(null) } .override("onError") { onResult(it.arg(0).toString()) }.build() ) @@ -81,7 +91,7 @@ class ConversationManager( instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(), messageId, - CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchMessageCallback")) + CallbackBuilder(getCallbackClass("FetchMessageCallback")) .override("onFetchMessageComplete") { param -> onSuccess(Message(param.arg(0))) } @@ -100,7 +110,7 @@ class ConversationManager( fetchMessageByServerId.invoke( instanceNonNull(), serverMessageIdentifier, - CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchMessageCallback")) + CallbackBuilder(getCallbackClass("FetchMessageCallback")) .override("onFetchMessageComplete") { param -> onSuccess(Message(param.arg(0))) } @@ -119,7 +129,7 @@ class ConversationManager( setObjectField("mServerMessageId", it) } }, - CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchMessagesByServerIdsCallback")) + CallbackBuilder(getCallbackClass("FetchMessagesByServerIdsCallback")) .override("onSuccess") { param -> onSuccess(param.arg>(0).mapNotNull { Message(it?.getObjectField("mMessage") ?: return@mapNotNull null) @@ -132,14 +142,14 @@ class ConversationManager( } fun clearConversation(conversationId: String, onSuccess: () -> Unit, onError: (error: String) -> Unit) { - val callback = CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) + val callback = CallbackBuilder(getCallbackClass("Callback")) .override("onSuccess") { onSuccess() } .override("onError") { onError(it.arg(0).toString()) }.build() clearConversation.invoke(instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(), callback) } fun getOneOnOneConversationIds(userIds: List, onSuccess: (List>) -> Unit, onError: (error: String) -> Unit) { - val callback = CallbackBuilder(context.mappings.getMappedClass("callbacks", "GetOneOnOneConversationIdsCallback")) + val callback = CallbackBuilder(getCallbackClass("GetOneOnOneConversationIdsCallback")) .override("onSuccess") { param -> onSuccess(param.arg>(0).map { SnapUUID(it.getObjectField("mUserId")).toString() to SnapUUID(it.getObjectField("mConversationId")).toString() diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/AbstractClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/AbstractClassMapper.kt index a5e82cce3..7601f1272 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/AbstractClassMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/AbstractClassMapper.kt @@ -1,8 +1,103 @@ package me.rhunk.snapenhance.mapper -abstract class AbstractClassMapper { +import android.util.Log +import com.google.gson.Gson +import com.google.gson.JsonObject + +abstract class AbstractClassMapper( + val mapperName: String +) { + lateinit var classLoader: ClassLoader + + private val gson = Gson() + private val values = mutableMapOf() private val mappers = mutableListOf Unit>() + private fun findClassSafe(className: String?) = runCatching { + classLoader.loadClass(className) + }.onFailure { + Log.e("Mapper", it.stackTraceToString()) + }.getOrNull() + + @Suppress("UNCHECKED_CAST") + inner class PropertyDelegate( + private val key: String, + defaultValue: Any? = null, + private val setter: (Any?) -> Unit = { values[key] = it }, + private val getter: (Any?) -> T? = { it as? T } + ) { + init { + values[key] = defaultValue + } + + operator fun getValue(thisRef: Any?, property: Any?): T? { + return getter(values[key]) + } + + operator fun setValue(thisRef: Any?, property: Any?, value: Any?) { + setter(value) + } + + fun set(value: String?) { + values[key] = value + } + + fun get(): T? { + return getter(values[key]) + } + + fun getAsClass(): Class<*>? { + return getter(values[key]) as? Class<*> + } + + fun getAsString(): String? { + return getter(values[key])?.toString() + } + + fun getClass(key: String): Class<*>? { + return (get() as? Map)?.let { + findClassSafe(it[key].toString()) + } + } + + override fun toString() = getter(values[key]).toString() + } + + fun string(key: String): PropertyDelegate = PropertyDelegate(key, null) + + fun classReference(key: String): PropertyDelegate> = PropertyDelegate(key, getter = { findClassSafe(it as? String) }) + + fun map(key: String, value: MutableMap = mutableMapOf()): PropertyDelegate> = PropertyDelegate(key, value) + + fun readFromJson(json: JsonObject) { + values.forEach { (key, _) -> + runCatching { + val jsonElement = json.get(key) ?: return@forEach + when (jsonElement) { + is JsonObject -> values[key] = gson.fromJson(jsonElement, HashMap::class.java) + else -> values[key] = jsonElement.asString + } + }.onFailure { + Log.e("Mapper","Failed to deserialize property $key") + } + } + } + + fun writeFromJson(json: JsonObject) { + values.forEach { (key, value) -> + runCatching { + when (value) { + is String -> json.addProperty(key, value) + is Class<*> -> json.addProperty(key, value.name) + is Map<*, *> -> json.add(key, gson.toJsonTree(value)) + else -> json.addProperty(key, value.toString()) + } + }.onFailure { + Log.e("Mapper","Failed to serialize property $key") + } + } + } + fun mapper(task: MapperContext.() -> Unit) { mappers.add(task) } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/Mapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt similarity index 58% rename from mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/Mapper.kt rename to mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt index cac0595db..9c87dc4de 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/Mapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt @@ -3,8 +3,8 @@ package me.rhunk.snapenhance.mapper import com.google.gson.JsonObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import me.rhunk.snapenhance.mapper.impl.* import org.jf.dexlib2.Opcodes import org.jf.dexlib2.dexbacked.DexBackedDexFile import org.jf.dexlib2.iface.ClassDef @@ -12,12 +12,33 @@ import java.io.BufferedInputStream import java.io.InputStream import java.util.zip.ZipFile import java.util.zip.ZipInputStream -import kotlin.reflect.KClass -class Mapper( - private vararg val mappers: KClass = arrayOf() +class ClassMapper( + private vararg val mappers: AbstractClassMapper = DEFAULT_MAPPERS, ) { private val classes = mutableListOf() + + companion object { + val DEFAULT_MAPPERS get() = arrayOf( + BCryptClassMapper(), + CallbackMapper(), + DefaultMediaItemMapper(), + MediaQualityLevelProviderMapper(), + OperaPageViewControllerMapper(), + PlusSubscriptionMapper(), + ScCameraSettingsMapper(), + StoryBoostStateMapper(), + FriendsFeedEventDispatcherMapper(), + CompositeConfigurationProviderMapper(), + ScoreUpdateMapper(), + FriendRelationshipChangerMapper(), + ViewBinderMapper(), + FriendingDataSourcesMapper(), + OperaViewerParamsMapper(), + ) + } + + fun loadApk(path: String) { val apkFile = ZipFile(path) val apkEntries = apkFile.entries().toList() @@ -50,20 +71,23 @@ class Mapper( } } - fun start(): JsonObject { - val mappers = mappers.map { it.java.constructors.first().newInstance() as AbstractClassMapper } + suspend fun run(): JsonObject { val context = MapperContext(classes.associateBy { it.type }) - runBlocking { - withContext(Dispatchers.IO) { - mappers.forEach { mapper -> - launch { - mapper.run(context) - } + withContext(Dispatchers.IO) { + mappers.forEach { mapper -> + launch { + mapper.run(context) } } } - return context.exportToJson() + val outputJson = JsonObject() + mappers.forEach { mapper -> + outputJson.add(mapper.mapperName, JsonObject().apply { + mapper.writeFromJson(this) + }) + } + return outputJson } } \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt index ecb44b717..c202aead6 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt @@ -1,7 +1,5 @@ package me.rhunk.snapenhance.mapper -import com.google.gson.GsonBuilder -import com.google.gson.JsonObject import org.jf.dexlib2.iface.ClassDef class MapperContext( @@ -19,40 +17,4 @@ class MapperContext( if (name == null) return null return classMap[name.toString()] } - - private val mappings = mutableMapOf() - - fun addMapping(key: String, vararg array: Pair) { - mappings[key] = array.toMap() - } - - fun addMapping(key: String, value: String) { - mappings[key] = value - } - - fun getStringMapping(key: String): String? { - return mappings[key] as? String - } - - fun getMapMapping(key: String): Map<*, *>? { - return mappings[key] as? Map<*, *> - } - - fun exportToJson(): JsonObject { - val gson = GsonBuilder().setPrettyPrinting().create() - val json = JsonObject() - for ((key, value) in mappings) { - when (value) { - is String -> json.addProperty(key, value) - is Map<*, *> -> { - val obj = JsonObject() - for ((k, v) in value) { - obj.add(k.toString(), gson.toJsonTree(v)) - } - json.add(key, obj) - } - } - } - return json - } } \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/BCryptClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/BCryptClassMapper.kt index f84cea8b3..d91d56815 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/BCryptClassMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/BCryptClassMapper.kt @@ -6,7 +6,10 @@ import me.rhunk.snapenhance.mapper.ext.getStaticConstructor import me.rhunk.snapenhance.mapper.ext.isFinal import org.jf.dexlib2.iface.instruction.formats.ArrayPayload -class BCryptClassMapper : AbstractClassMapper() { +class BCryptClassMapper : AbstractClassMapper("BCryptClass") { + val classReference = classReference("class") + val hashMethod = string("hashMethod") + init { mapper { for (clazz in classes) { @@ -17,17 +20,15 @@ class BCryptClassMapper : AbstractClassMapper() { } if (isBcryptClass == true) { - val hashMethod = clazz.methods.first { + val hashDexMethod = clazz.methods.first { it.parameterTypes.size == 2 && it.parameterTypes[0] == "Ljava/lang/String;" && it.parameterTypes[1] == "Ljava/lang/String;" && it.returnType == "Ljava/lang/String;" } - addMapping("BCrypt", - "class" to clazz.getClassName(), - "hashMethod" to hashMethod.name - ) + hashMethod.set(hashDexMethod.name) + classReference.set(clazz.getClassName()) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt index c48a2dcb1..fda7b3f65 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt @@ -4,11 +4,12 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getSuperClassName import me.rhunk.snapenhance.mapper.ext.isFinal -import org.jf.dexlib2.Opcode import org.jf.dexlib2.iface.instruction.formats.Instruction21t import org.jf.dexlib2.iface.instruction.formats.Instruction22t -class CallbackMapper : AbstractClassMapper() { +class CallbackMapper : AbstractClassMapper("Callbacks") { + val callbacks = map("callbacks") + init { mapper { val callbackClasses = classes.filter { clazz -> @@ -32,7 +33,7 @@ class CallbackMapper : AbstractClassMapper() { it.getSuperClassName()!!.substringAfterLast("/") to it.getClassName() } - addMapping("callbacks", *callbackClasses.toTypedArray()) + callbacks.get()?.putAll(callbackClasses) } } } \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt index 9cc4038eb..7aeb671c0 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt @@ -1,14 +1,32 @@ package me.rhunk.snapenhance.mapper.impl import me.rhunk.snapenhance.mapper.AbstractClassMapper -import me.rhunk.snapenhance.mapper.ext.* +import me.rhunk.snapenhance.mapper.ext.findConstString +import me.rhunk.snapenhance.mapper.ext.getClassName +import me.rhunk.snapenhance.mapper.ext.hasStaticConstructorString +import me.rhunk.snapenhance.mapper.ext.isEnum import org.jf.dexlib2.iface.instruction.formats.Instruction21c import org.jf.dexlib2.iface.instruction.formats.Instruction35c import org.jf.dexlib2.iface.reference.FieldReference import org.jf.dexlib2.iface.reference.MethodReference import java.lang.reflect.Modifier -class CompositeConfigurationProviderMapper : AbstractClassMapper() { +class CompositeConfigurationProviderMapper : AbstractClassMapper("CompositeConfigurationProvider") { + val classReference = classReference("class") + val observeProperty = string("observeProperty") + val getProperty = string("getProperty") + val configEnumMapping = mapOf( + "class" to classReference("enumClass"), + "getValue" to string("enumGetValue"), + "getCategory" to string("enumGetCategory"), + "defaultValueField" to string("enumDefaultValueField") + ) + val appExperimentProvider = mapOf( + "class" to classReference("appExperimentProviderClass"), + "getBooleanAppExperimentClass" to classReference("getBooleanAppExperimentClass"), + "hasExperimentMethod" to string("hasExperimentMethod") + ) + init { mapper { for (classDef in classes) { @@ -68,24 +86,21 @@ class CompositeConfigurationProviderMapper : AbstractClassMapper() { it.type == "Ljava/lang/Object;" } - addMapping("CompositeConfigurationProvider", - "class" to classDef.getClassName(), - "observeProperty" to observePropertyMethod.name, - "getProperty" to getPropertyMethod.name, - "enum" to mapOf( - "class" to configEnumInterface.getClassName(), - "getValue" to enumGetDefaultValueMethod.name, - "getCategory" to enumGetCategoryMethod.name, - "defaultValueField" to defaultValueField.name - ), - "appExperimentProvider" to (hasExperimentMethodReference?.let { - mapOf( - "class" to getClass(it.definingClass)?.getClassName(), - "GetBooleanAppExperimentClass" to getBooleanAppExperimentClass, - "hasExperimentMethod" to hasExperimentMethodReference.name - ) - }) - ) + classReference.set(classDef.getClassName()) + observeProperty.set(observePropertyMethod.name) + getProperty.set(getPropertyMethod.name) + + configEnumMapping["class"]?.set(configEnumInterface.getClassName()) + configEnumMapping["getValue"]?.set(enumGetDefaultValueMethod.name) + configEnumMapping["getCategory"]?.set(enumGetCategoryMethod.name) + configEnumMapping["defaultValueField"]?.set(defaultValueField.name) + + hasExperimentMethodReference?.let { + appExperimentProvider["class"]?.set(getClass(it.definingClass)?.getClassName()) + appExperimentProvider["getBooleanAppExperimentClass"]?.set(getBooleanAppExperimentClass) + appExperimentProvider["hasExperimentMethod"]?.set(hasExperimentMethodReference.name) + } + return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/DefaultMediaItemMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/DefaultMediaItemMapper.kt index ba0c9fd34..6bdca6831 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/DefaultMediaItemMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/DefaultMediaItemMapper.kt @@ -5,16 +5,21 @@ import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.isAbstract -class DefaultMediaItemMapper : AbstractClassMapper() { +class DefaultMediaItemMapper : AbstractClassMapper("DefaultMediaItem") { + val cameraRollMediaId = classReference("cameraRollMediaIdClass") + val durationMsField = string("durationMsField") + val defaultMediaItem = classReference("defaultMediaItemClass") + init { mapper { for (clazz in classes) { if (clazz.methods.find { it.name == "toString" }?.implementation?.findConstString("CameraRollMediaId", contains = true) != true) { continue } - val durationMsField = clazz.fields.firstOrNull { it.type == "J" } ?: continue + val durationMsDexField = clazz.fields.firstOrNull { it.type == "J" } ?: continue - addMapping("CameraRollMediaId", "class" to clazz.getClassName(), "durationMsField" to durationMsField.name) + cameraRollMediaId.set(clazz.getClassName()) + durationMsField.set(durationMsDexField.name) return@mapper } } @@ -29,7 +34,7 @@ class DefaultMediaItemMapper : AbstractClassMapper() { val constructorParameters = clazz.directMethods.firstOrNull { it.name == "" }?.parameterTypes ?: continue if (constructorParameters.size < 6 || constructorParameters[5] != "J") continue - addMapping("DefaultMediaItem", clazz.getClassName()) + defaultMediaItem.set(clazz.getClassName()) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendRelationshipChangerMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendRelationshipChangerMapper.kt index d122b9556..01687b523 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendRelationshipChangerMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendRelationshipChangerMapper.kt @@ -5,12 +5,16 @@ import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.isEnum -class FriendRelationshipChangerMapper : AbstractClassMapper() { +class FriendRelationshipChangerMapper : AbstractClassMapper("FriendRelationshipChanger") { + val classReference = classReference("class") + val addFriendMethod = string("addFriendMethod") + val removeFriendMethod = string("removeFriendMethod") + init { mapper { for (classDef in classes) { classDef.methods.firstOrNull { it.name == "" }?.implementation?.findConstString("FriendRelationshipChangerImpl")?.takeIf { it } ?: continue - val addFriendMethod = classDef.methods.first { + val addFriendDexMethod = classDef.methods.first { it.parameterTypes.size > 4 && getClass(it.parameterTypes[1])?.isEnum() == true && getClass(it.parameterTypes[2])?.isEnum() == true && @@ -18,7 +22,7 @@ class FriendRelationshipChangerMapper : AbstractClassMapper() { it.parameters[4].type == "Ljava/lang/String;" } - val removeFriendMethod = classDef.methods.first { + val removeFriendDexMethod = classDef.methods.firstOrNull { it.parameterTypes.size == 5 && it.parameterTypes[0] == "Ljava/lang/String;" && getClass(it.parameterTypes[1])?.isEnum() == true && @@ -26,11 +30,12 @@ class FriendRelationshipChangerMapper : AbstractClassMapper() { it.parameterTypes[3] == "Ljava/lang/String;" } - addMapping("FriendRelationshipChanger", - "class" to classDef.getClassName(), - "addFriendMethod" to addFriendMethod.name, - "removeFriendMethod" to removeFriendMethod.name - ) + this@FriendRelationshipChangerMapper.apply { + classReference.set(classDef.getClassName()) + addFriendMethod.set(addFriendDexMethod.name) + removeFriendMethod.set(removeFriendDexMethod?.name) + } + return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendingDataSourcesMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendingDataSourcesMapper.kt index 219fb4431..6f435a2a5 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendingDataSourcesMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendingDataSourcesMapper.kt @@ -5,7 +5,10 @@ import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.searchNextFieldReference -class FriendingDataSourcesMapper: AbstractClassMapper() { +class FriendingDataSourcesMapper: AbstractClassMapper("FriendingDataSources") { + val classReference = classReference("class") + val quickAddSourceListField = string("quickAddSourceListField") + init { mapper { for (classDef in classes) { @@ -15,13 +18,11 @@ class FriendingDataSourcesMapper: AbstractClassMapper() { val toStringMethod = classDef.methods.firstOrNull { it.name == "toString" } ?: continue if (toStringMethod.implementation?.findConstString("quickaddSource", contains = true) != true) continue - val quickAddSourceListField = toStringMethod.implementation?.searchNextFieldReference("quickaddSource", contains = true) + val quickAddSourceListDexField = toStringMethod.implementation?.searchNextFieldReference("quickaddSource", contains = true) ?: continue - addMapping("FriendingDataSources", - "class" to classDef.getClassName(), - "quickAddSourceListField" to quickAddSourceListField.name - ) + classReference.set(classDef.getClassName()) + quickAddSourceListField.set(quickAddSourceListDexField.name) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendsFeedEventDispatcherMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendsFeedEventDispatcherMapper.kt index 74619f45a..3bd45001a 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendsFeedEventDispatcherMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendsFeedEventDispatcherMapper.kt @@ -5,7 +5,10 @@ import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName -class FriendsFeedEventDispatcherMapper : AbstractClassMapper() { +class FriendsFeedEventDispatcherMapper : AbstractClassMapper("FriendsFeedEventDispatcher") { + val classReference = classReference("class") + val viewModelField = string("viewModelField") + init { mapper { for (clazz in classes) { @@ -13,15 +16,13 @@ class FriendsFeedEventDispatcherMapper : AbstractClassMapper() { val onItemLongPress = clazz.methods.first { it.name == "onItemLongPress" } val viewHolderContainerClass = getClass(onItemLongPress.parameterTypes[0]) ?: continue - val viewModelField = viewHolderContainerClass.fields.firstOrNull { field -> + val viewModelDexField = viewHolderContainerClass.fields.firstOrNull { field -> val typeClass = getClass(field.type) ?: return@firstOrNull false typeClass.methods.firstOrNull {it.name == "toString"}?.implementation?.findConstString("FriendFeedItemViewModel", contains = true) == true }?.name ?: continue - addMapping("FriendsFeedEventDispatcher", - "class" to clazz.getClassName(), - "viewModelField" to viewModelField - ) + classReference.set(clazz.getClassName()) + viewModelField.set(viewModelDexField) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/MediaQualityLevelProviderMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/MediaQualityLevelProviderMapper.kt index 1c74e6a76..eff27bfd8 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/MediaQualityLevelProviderMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/MediaQualityLevelProviderMapper.kt @@ -7,7 +7,10 @@ import me.rhunk.snapenhance.mapper.ext.isAbstract import me.rhunk.snapenhance.mapper.ext.isEnum import org.jf.dexlib2.AccessFlags -class MediaQualityLevelProviderMapper : AbstractClassMapper() { +class MediaQualityLevelProviderMapper : AbstractClassMapper("MediaQualityLevelProvider") { + val mediaQualityLevelProvider = classReference("mediaQualityLevelProvider") + val mediaQualityLevelProviderMethod = string("mediaQualityLevelProviderMethod") + init { var enumQualityLevel : String? = null @@ -20,7 +23,6 @@ class MediaQualityLevelProviderMapper : AbstractClassMapper() { break; } } - addMapping("EnumQualityLevel", enumQualityLevel ?: return@mapper) } mapper { @@ -31,10 +33,8 @@ class MediaQualityLevelProviderMapper : AbstractClassMapper() { if (clazz.fields.none { it.accessFlags and AccessFlags.TRANSIENT.value != 0 }) continue clazz.methods.firstOrNull { it.returnType == "L$enumQualityLevel;" }?.let { - addMapping("MediaQualityLevelProvider", - "class" to clazz.getClassName(), - "method" to it.name - ) + mediaQualityLevelProvider.set(clazz.getClassName()) + mediaQualityLevelProviderMethod.set(it.name) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaPageViewControllerMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaPageViewControllerMapper.kt index 41025a4ac..db25f900c 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaPageViewControllerMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaPageViewControllerMapper.kt @@ -7,7 +7,13 @@ import me.rhunk.snapenhance.mapper.ext.hasStaticConstructorString import me.rhunk.snapenhance.mapper.ext.isAbstract import me.rhunk.snapenhance.mapper.ext.isEnum -class OperaPageViewControllerMapper : AbstractClassMapper() { +class OperaPageViewControllerMapper : AbstractClassMapper("OperaPageViewController") { + val classReference = classReference("class") + val viewStateField = string("viewStateField") + val layerListField = string("layerListField") + val onDisplayStateChange = string("onDisplayStateChange") + val onDisplayStateChangeGesture = string("onDisplayStateChangeGesture") + init { mapper { for (clazz in classes) { @@ -16,37 +22,35 @@ class OperaPageViewControllerMapper : AbstractClassMapper() { continue } - val viewStateField = clazz.fields.first { field -> + val viewStateDexField = clazz.fields.first { field -> val fieldClass = getClass(field.type) ?: return@first false fieldClass.isEnum() && fieldClass.hasStaticConstructorString("FULLY_DISPLAYED") } - val layerListField = clazz.fields.first { it.type == "Ljava/util/ArrayList;" } + val layerListDexField = clazz.fields.first { it.type == "Ljava/util/ArrayList;" } - val onDisplayStateChange = clazz.methods.firstOrNull { + val onDisplayStateChangeDexMethod = clazz.methods.firstOrNull { if (it.returnType != "V" || it.parameterTypes.size != 1) return@firstOrNull false val firstParameterType = getClass(it.parameterTypes[0]) ?: return@firstOrNull false if (firstParameterType.type == clazz.type || !firstParameterType.isAbstract()) return@firstOrNull false //check if the class contains a field with the enumViewStateClass type firstParameterType.fields.any { field -> - field.type == viewStateField.type + field.type == viewStateDexField.type } } - val onDisplayStateChangeGesture = clazz.methods.first { + val onDisplayStateChangeGestureDexMethod = clazz.methods.first { if (it.returnType != "V" || it.parameterTypes.size != 2) return@first false val firstParameterType = getClass(it.parameterTypes[0]) ?: return@first false val secondParameterType = getClass(it.parameterTypes[1]) ?: return@first false firstParameterType.isEnum() && secondParameterType.isEnum() } - addMapping("OperaPageViewController", - "class" to clazz.getClassName(), - "viewStateField" to viewStateField.name, - "layerListField" to layerListField.name, - "onDisplayStateChange" to onDisplayStateChange?.name, - "onDisplayStateChangeGesture" to onDisplayStateChangeGesture.name - ) + classReference.set(clazz.getClassName()) + viewStateField.set(viewStateDexField.name) + layerListField.set(layerListDexField.name) + onDisplayStateChange.set(onDisplayStateChangeDexMethod?.name) + onDisplayStateChangeGesture.set(onDisplayStateChangeGestureDexMethod.name) return@mapper } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt index d6de7b768..670bff0e8 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt @@ -6,14 +6,17 @@ import me.rhunk.snapenhance.mapper.ext.getClassName import org.jf.dexlib2.iface.instruction.formats.Instruction35c import org.jf.dexlib2.iface.reference.MethodReference -class OperaViewerParamsMapper : AbstractClassMapper() { +class OperaViewerParamsMapper : AbstractClassMapper("OperaViewerParams") { + val classReference = classReference("class") + val putMethod = string("putMethod") + init { mapper { for (classDef in classes) { classDef.fields.firstOrNull { it.type == "Ljava/util/concurrent/ConcurrentHashMap;" } ?: continue if (classDef.methods.firstOrNull { it.name == "toString" }?.implementation?.findConstString("Params") != true) continue - val putMethod = classDef.methods.firstOrNull { method -> + val putDexMethod = classDef.methods.firstOrNull { method -> method.implementation?.instructions?.any { val instruction = it as? Instruction35c ?: return@any false val reference = instruction.reference as? MethodReference ?: return@any false @@ -21,10 +24,9 @@ class OperaViewerParamsMapper : AbstractClassMapper() { } == true } ?: return@mapper - addMapping("OperaViewerParams", - "class" to classDef.getClassName(), - "putMethod" to putMethod.name - ) + classReference.set(classDef.getClassName()) + putMethod.set(putDexMethod.name) + return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/PlusSubscriptionMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/PlusSubscriptionMapper.kt index 6f5a3667c..a14ab9645 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/PlusSubscriptionMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/PlusSubscriptionMapper.kt @@ -4,24 +4,26 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName -class PlusSubscriptionMapper : AbstractClassMapper(){ +class PlusSubscriptionMapper : AbstractClassMapper("PlusSubscription"){ + val classReference = classReference("class") + init { mapper { for (clazz in classes) { if (clazz.directMethods.filter { it.name == "" }.none { - it.parameters.size == 4 && - it.parameterTypes[0] == "I" && - it.parameterTypes[1] == "I" && - it.parameterTypes[2] == "J" && - it.parameterTypes[3] == "J" - }) continue + it.parameterTypes.size > 3 && + it.parameterTypes[0] == "I" && + it.parameterTypes[1] == "I" && + it.parameterTypes[2] == "J" && + it.parameterTypes[3] == "J" + }) continue val isPlusSubscriptionInfoClass = clazz.virtualMethods.firstOrNull { it.name == "toString" }?.implementation?.let { it.findConstString("SubscriptionInfo", contains = true) && it.findConstString("expirationTimeMillis", contains = true) } if (isPlusSubscriptionInfoClass == true) { - addMapping("SubscriptionInfoClass", clazz.getClassName()) + classReference.set(clazz.getClassName()) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScCameraSettingsMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScCameraSettingsMapper.kt index 4793ad45a..2c0a77411 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScCameraSettingsMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScCameraSettingsMapper.kt @@ -6,7 +6,9 @@ import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getStaticConstructor import me.rhunk.snapenhance.mapper.ext.isEnum -class ScCameraSettingsMapper : AbstractClassMapper() { +class ScCameraSettingsMapper : AbstractClassMapper("ScCameraSettings") { + val classReference = classReference("class") + init { mapper { for (clazz in classes) { @@ -15,7 +17,7 @@ class ScCameraSettingsMapper : AbstractClassMapper() { val firstParameter = getClass(firstConstructor.parameterTypes[0]) ?: continue if (!firstParameter.isEnum() || firstParameter.getStaticConstructor()?.implementation?.findConstString("CONTINUOUS_PICTURE") != true) continue - addMapping("ScCameraSettings", clazz.getClassName()) + classReference.set(clazz.getClassName()) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScoreUpdateMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScoreUpdateMapper.kt index dba8900bc..41d42dd12 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScoreUpdateMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScoreUpdateMapper.kt @@ -4,7 +4,9 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName -class ScoreUpdateMapper : AbstractClassMapper() { +class ScoreUpdateMapper : AbstractClassMapper("ScoreUpdate") { + val classReference = classReference("class") + init { mapper { for (classDef in classes) { @@ -18,7 +20,7 @@ class ScoreUpdateMapper : AbstractClassMapper() { it.name == "toString" }?.implementation?.findConstString("Friend.sq:selectFriendUserScoresNeedToUpdate") != true) continue - addMapping("ScoreUpdate", classDef.getClassName()) + classReference.set(classDef.getClassName()) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StoryBoostStateMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StoryBoostStateMapper.kt index bbc409ee8..bab2a82fe 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StoryBoostStateMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StoryBoostStateMapper.kt @@ -4,7 +4,9 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName -class StoryBoostStateMapper : AbstractClassMapper() { +class StoryBoostStateMapper : AbstractClassMapper("StoryBoostState") { + val classReference = classReference("class") + init { mapper { for (clazz in classes) { @@ -14,7 +16,7 @@ class StoryBoostStateMapper : AbstractClassMapper() { if (clazz.methods.firstOrNull { it.name == "toString" }?.implementation?.findConstString("StoryBoostState", contains = true) != true) continue - addMapping("StoryBoostStateClass", clazz.getClassName()) + classReference.set(clazz.getClassName()) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ViewBinderMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ViewBinderMapper.kt index 311b2b063..95904bbd3 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ViewBinderMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ViewBinderMapper.kt @@ -6,13 +6,17 @@ import me.rhunk.snapenhance.mapper.ext.isAbstract import me.rhunk.snapenhance.mapper.ext.isInterface import java.lang.reflect.Modifier -class ViewBinderMapper : AbstractClassMapper() { +class ViewBinderMapper : AbstractClassMapper("ViewBinder") { + val classReference = classReference("class") + val bindMethod = string("bindMethod") + val getViewMethod = string("getViewMethod") + init { mapper { for (clazz in classes) { if (!clazz.isAbstract() || clazz.isInterface()) continue - val getViewMethod = clazz.methods.firstOrNull { it.returnType == "Landroid/view/View;" && it.parameterTypes.size == 0 } ?: continue + val getViewDexMethod = clazz.methods.firstOrNull { it.returnType == "Landroid/view/View;" && it.parameterTypes.size == 0 } ?: continue // update view clazz.methods.filter { @@ -21,17 +25,15 @@ class ViewBinderMapper : AbstractClassMapper() { if (it.size != 1) return@also }.firstOrNull() ?: continue - val bindMethod = clazz.methods.filter { + val bindDexMethod = clazz.methods.filter { Modifier.isAbstract(it.accessFlags) && it.parameterTypes.size == 2 && it.parameterTypes[0] == it.parameterTypes[1] && it.returnType == "V" }.also { if (it.size != 1) return@also }.firstOrNull() ?: continue - addMapping("ViewBinder", - "class" to clazz.getClassName(), - "bindMethod" to bindMethod.name, - "getViewMethod" to getViewMethod.name - ) + classReference.set(clazz.getClassName()) + bindMethod.set(bindDexMethod.name) + getViewMethod.set(getViewDexMethod.name) return@mapper } } diff --git a/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt b/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt index 9a0eaf5a4..e432bcdc5 100644 --- a/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt +++ b/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt @@ -1,7 +1,8 @@ package me.rhunk.snapenhance.mapper.tests import com.google.gson.GsonBuilder -import me.rhunk.snapenhance.mapper.Mapper +import kotlinx.coroutines.runBlocking +import me.rhunk.snapenhance.mapper.ClassMapper import me.rhunk.snapenhance.mapper.impl.* import org.junit.Test import java.io.File @@ -10,28 +11,14 @@ import java.io.File class TestMappings { @Test fun testMappings() { - val mapper = Mapper( - BCryptClassMapper::class, - CallbackMapper::class, - DefaultMediaItemMapper::class, - MediaQualityLevelProviderMapper::class, - OperaPageViewControllerMapper::class, - PlusSubscriptionMapper::class, - ScCameraSettingsMapper::class, - StoryBoostStateMapper::class, - FriendsFeedEventDispatcherMapper::class, - CompositeConfigurationProviderMapper::class, - ScoreUpdateMapper::class, - FriendRelationshipChangerMapper::class, - ViewBinderMapper::class, - FriendingDataSourcesMapper::class, - OperaViewerParamsMapper::class, - ) + val classMapper = ClassMapper() val gson = GsonBuilder().setPrettyPrinting().create() val apkFile = File(System.getenv("SNAPCHAT_APK")!!) - mapper.loadApk(apkFile.absolutePath) - val result = mapper.start() - println("Mappings: ${gson.toJson(result)}") + classMapper.loadApk(apkFile.absolutePath) + runBlocking { + val result = classMapper.run() + println("Mappings: ${gson.toJson(result)}") + } } } From c16af141e640c4eb8f71e7a5a7d4b9601417a7d1 Mon Sep 17 00:00:00 2001 From: Caner Karaca <37447503+CanerKaraca23@users.noreply.github.com> Date: Fri, 12 Jan 2024 22:35:50 +0300 Subject: [PATCH 46/53] chore: update dependencies (#565) * Update libs.versions.toml * fix: proguard rules --------- Co-authored-by: rhunk <101876869+rhunk@users.noreply.github.com> --- app/proguard-rules.pro | 2 +- gradle/libs.versions.toml | 24 +++++++++---------- manager/proguard-rules.pro | 2 +- .../manager/patch/util/DexLibExt.kt | 14 +++++------ .../rhunk/snapenhance/mapper/ClassMapper.kt | 8 +++---- .../rhunk/snapenhance/mapper/MapperContext.kt | 4 ++-- .../snapenhance/mapper/ext/DexClassDef.kt | 4 ++-- .../rhunk/snapenhance/mapper/ext/DexMethod.kt | 10 ++++---- .../mapper/impl/BCryptClassMapper.kt | 4 ++-- .../snapenhance/mapper/impl/CallbackMapper.kt | 6 ++--- .../CompositeConfigurationProviderMapper.kt | 10 ++++---- .../impl/MediaQualityLevelProviderMapper.kt | 4 ++-- .../mapper/impl/OperaViewerParamsMapper.kt | 6 ++--- .../snapenhance/mapper/tests/TestMappings.kt | 2 +- 14 files changed, 50 insertions(+), 50 deletions(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 3d8b2a6b2..56833c926 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -3,7 +3,7 @@ -keep enum * { *; } --keep class org.jf.dexlib2.** { *; } +-keep class com.android.tools.smali.dexlib2.** { *; } -keep class org.mozilla.javascript.** { *; } -keep class androidx.compose.material.icons.** { *; } -keep class androidx.compose.material3.R$* { *; } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b4a31861e..ea8ac8c4b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,28 +1,28 @@ [versions] -agp = "8.2.0" -apksig = "8.2.0" +agp = "8.2.1" +apksig = "8.2.1" libsu = "5.2.2" -guava = "32.1.3-jre" -jsoup = "1.17.1" -kotlin = "1.9.21" +guava = "33.0.0-jre" +jsoup = "1.17.2" +kotlin = "1.9.22" kotlinx-coroutines-android = "1.7.3" -compose-compiler = "1.5.6" +compose-compiler = "1.5.8" activity-ktx = "1.8.2" androidx-documentfile = "1.1.0-alpha01" coil-compose = "2.5.0" navigation-compose = "2.7.6" -osmdroid-android = "6.1.17" +osmdroid-android = "6.1.18" recyclerview = "1.3.2" compose-bom = "2023.10.01" bcprov-jdk18on = "1.77" -dexlib2 = "2.5.2" +dexlib2 = "3.0.3" ffmpeg-kit = "5.1.LTS" # DO NOT UPDATE FFMPEG-KIT TO "5.1" it breaks stuff :3 gson = "2.10.1" -junit = "4.13.2" +junit = "5.10.1" material3 = "1.1.2" -okhttp = "5.0.0-alpha.11" +okhttp = "5.0.0-alpha.12" rhino = "1.7.14" @@ -43,12 +43,12 @@ coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose coil-video = { module = "io.coil-kt:coil-video", version.ref = "coil-compose" } libsu = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" } coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" } -dexlib2 = { group = "org.smali", name = "dexlib2", version.ref = "dexlib2" } +dexlib2 = { group = "com.android.tools.smali", name = "smali-dexlib2", version.ref = "dexlib2" } ffmpeg-kit = { group = "com.arthenica", name = "ffmpeg-kit-full-gpl", version.ref = "ffmpeg-kit" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } guava = { module = "com.google.guava:guava", version.ref = "guava" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } -junit = { module = "junit:junit", version.ref = "junit" } +junit = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } osmdroid-android = { group = "org.osmdroid", name = "osmdroid-android", version.ref = "osmdroid-android" } recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" } diff --git a/manager/proguard-rules.pro b/manager/proguard-rules.pro index 71e979c34..717606da5 100644 --- a/manager/proguard-rules.pro +++ b/manager/proguard-rules.pro @@ -1,5 +1,5 @@ -dontwarn com.google.errorprone.annotations.** -dontwarn com.google.auto.value.** -keep enum * { *; } --keep class org.jf.dexlib2.** { *; } +-keep class com.android.tools.smali.dexlib2.** { *; } -keep class me.rhunk.snapenhance.manager.ui.tab.** { *; } \ No newline at end of file diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/patch/util/DexLibExt.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/patch/util/DexLibExt.kt index 97f01a0b4..d0bdf350c 100644 --- a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/patch/util/DexLibExt.kt +++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/patch/util/DexLibExt.kt @@ -1,12 +1,12 @@ package me.rhunk.snapenhance.manager.patch.util -import org.jf.dexlib2.Opcodes -import org.jf.dexlib2.dexbacked.DexBackedDexFile -import org.jf.dexlib2.iface.DexFile -import org.jf.dexlib2.iface.reference.StringReference -import org.jf.dexlib2.writer.io.FileDataStore -import org.jf.dexlib2.writer.pool.DexPool -import org.jf.dexlib2.writer.pool.StringPool +import com.android.tools.smali.dexlib2.Opcodes +import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile +import com.android.tools.smali.dexlib2.iface.DexFile +import com.android.tools.smali.dexlib2.iface.reference.StringReference +import com.android.tools.smali.dexlib2.writer.io.FileDataStore +import com.android.tools.smali.dexlib2.writer.pool.DexPool +import com.android.tools.smali.dexlib2.writer.pool.StringPool import java.io.BufferedInputStream import java.io.File import java.io.InputStream diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt index 9c87dc4de..283d64d35 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt @@ -5,9 +5,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.rhunk.snapenhance.mapper.impl.* -import org.jf.dexlib2.Opcodes -import org.jf.dexlib2.dexbacked.DexBackedDexFile -import org.jf.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.Opcodes +import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile +import com.android.tools.smali.dexlib2.iface.ClassDef import java.io.BufferedInputStream import java.io.InputStream import java.util.zip.ZipFile @@ -90,4 +90,4 @@ class ClassMapper( } return outputJson } -} \ No newline at end of file +} diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt index c202aead6..7d74a9588 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt @@ -1,6 +1,6 @@ package me.rhunk.snapenhance.mapper -import org.jf.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.ClassDef class MapperContext( private val classMap: Map @@ -17,4 +17,4 @@ class MapperContext( if (name == null) return null return classMap[name.toString()] } -} \ No newline at end of file +} diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ext/DexClassDef.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ext/DexClassDef.kt index 832d4cef4..aea13738d 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ext/DexClassDef.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ext/DexClassDef.kt @@ -1,7 +1,7 @@ package me.rhunk.snapenhance.mapper.ext -import org.jf.dexlib2.AccessFlags -import org.jf.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.iface.ClassDef fun ClassDef.isEnum(): Boolean = accessFlags and AccessFlags.ENUM.value != 0 fun ClassDef.isAbstract(): Boolean = accessFlags and AccessFlags.ABSTRACT.value != 0 diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ext/DexMethod.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ext/DexMethod.kt index 29b0af3d1..1c1ce40ab 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ext/DexMethod.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ext/DexMethod.kt @@ -1,10 +1,10 @@ package me.rhunk.snapenhance.mapper.ext -import org.jf.dexlib2.iface.MethodImplementation -import org.jf.dexlib2.iface.instruction.formats.Instruction21c -import org.jf.dexlib2.iface.instruction.formats.Instruction22c -import org.jf.dexlib2.iface.reference.FieldReference -import org.jf.dexlib2.iface.reference.StringReference +import com.android.tools.smali.dexlib2.iface.MethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction21c +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22c +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.StringReference fun MethodImplementation.findConstString(string: String, contains: Boolean = false): Boolean = instructions.filterIsInstance(Instruction21c::class.java).any { (it.reference as? StringReference)?.string?.let { str -> diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/BCryptClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/BCryptClassMapper.kt index d91d56815..20d5f9168 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/BCryptClassMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/BCryptClassMapper.kt @@ -4,7 +4,7 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getStaticConstructor import me.rhunk.snapenhance.mapper.ext.isFinal -import org.jf.dexlib2.iface.instruction.formats.ArrayPayload +import com.android.tools.smali.dexlib2.iface.instruction.formats.ArrayPayload class BCryptClassMapper : AbstractClassMapper("BCryptClass") { val classReference = classReference("class") @@ -34,4 +34,4 @@ class BCryptClassMapper : AbstractClassMapper("BCryptClass") { } } } -} \ No newline at end of file +} diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt index fda7b3f65..b46c0c9b8 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt @@ -4,8 +4,8 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getSuperClassName import me.rhunk.snapenhance.mapper.ext.isFinal -import org.jf.dexlib2.iface.instruction.formats.Instruction21t -import org.jf.dexlib2.iface.instruction.formats.Instruction22t +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction21t +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22t class CallbackMapper : AbstractClassMapper("Callbacks") { val callbacks = map("callbacks") @@ -36,4 +36,4 @@ class CallbackMapper : AbstractClassMapper("Callbacks") { callbacks.get()?.putAll(callbackClasses) } } -} \ No newline at end of file +} diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt index 7aeb671c0..009ba6dd1 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt @@ -5,10 +5,10 @@ import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.hasStaticConstructorString import me.rhunk.snapenhance.mapper.ext.isEnum -import org.jf.dexlib2.iface.instruction.formats.Instruction21c -import org.jf.dexlib2.iface.instruction.formats.Instruction35c -import org.jf.dexlib2.iface.reference.FieldReference -import org.jf.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction21c +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference import java.lang.reflect.Modifier class CompositeConfigurationProviderMapper : AbstractClassMapper("CompositeConfigurationProvider") { @@ -105,4 +105,4 @@ class CompositeConfigurationProviderMapper : AbstractClassMapper("CompositeConfi } } } -} \ No newline at end of file +} diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/MediaQualityLevelProviderMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/MediaQualityLevelProviderMapper.kt index eff27bfd8..7f27dd1ee 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/MediaQualityLevelProviderMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/MediaQualityLevelProviderMapper.kt @@ -5,7 +5,7 @@ import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.hasStaticConstructorString import me.rhunk.snapenhance.mapper.ext.isAbstract import me.rhunk.snapenhance.mapper.ext.isEnum -import org.jf.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.AccessFlags class MediaQualityLevelProviderMapper : AbstractClassMapper("MediaQualityLevelProvider") { val mediaQualityLevelProvider = classReference("mediaQualityLevelProvider") @@ -40,4 +40,4 @@ class MediaQualityLevelProviderMapper : AbstractClassMapper("MediaQualityLevelPr } } } -} \ No newline at end of file +} diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt index 670bff0e8..1694faa57 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt @@ -3,8 +3,8 @@ package me.rhunk.snapenhance.mapper.impl import me.rhunk.snapenhance.mapper.AbstractClassMapper import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName -import org.jf.dexlib2.iface.instruction.formats.Instruction35c -import org.jf.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c +import com.android.tools.smali.dexlib2.iface.reference.MethodReference class OperaViewerParamsMapper : AbstractClassMapper("OperaViewerParams") { val classReference = classReference("class") @@ -31,4 +31,4 @@ class OperaViewerParamsMapper : AbstractClassMapper("OperaViewerParams") { } } } -} \ No newline at end of file +} diff --git a/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt b/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt index e432bcdc5..9a4b02a3b 100644 --- a/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt +++ b/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt @@ -4,7 +4,7 @@ import com.google.gson.GsonBuilder import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.mapper.ClassMapper import me.rhunk.snapenhance.mapper.impl.* -import org.junit.Test +import org.junit.jupiter.api.Test import java.io.File From 5328dec6209bfb6596267acc736cd6ed6747bd45 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Fri, 12 Jan 2024 22:45:00 +0100 Subject: [PATCH 47/53] chore: bug report template --- .github/ISSUE_TEMPLATE/bug_report.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d845cddad..9b524529c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: "Bug report" description: "Report an issue to help the project improve." -title: "bug: TITLE" +title: "TITLE" labels: - "bug" body: @@ -40,13 +40,20 @@ body: placeholder: "ex. 12.35.0.45" validations: required: true + - type: input + id: snapenhance-version + attributes: + label: "SnapEnhance Version" + description: "On which SnapEnhance version is this happening?" + placeholder: "ex. 1.2.5" + validations: + required: true - type: checkboxes id: terms attributes: label: "Agreement" - description: "By creating this issue, I agree to the following terms:" + description: "**By creating this issue, I agree to the following terms:**" options: - - label: "I am using the latest stable SnapEnhance version." - label: "This is not a bug regarding Snapchat+." - label: "I have provided a detailed description of the issue." - label: "I have attached a log if deemed neccessary." From 5d8cdc3cfce8408ade7d2abbc885659383d19c2a Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sat, 13 Jan 2024 17:39:08 +0100 Subject: [PATCH 48/53] fix: hide story sections --- common/src/main/assets/lang/en_US.json | 24 ++-- .../snapenhance/common/config/impl/Global.kt | 2 +- .../common/config/impl/UserInterfaceTweaks.kt | 3 +- .../snapenhance/common/data/SnapEnums.kt | 16 +++ .../core/features/impl/MixerStories.kt | 119 ++++++++++++++++++ .../snapenhance/core/features/impl/Stories.kt | 101 --------------- .../core/features/impl/ui/UITweaks.kt | 26 +--- .../core/manager/impl/FeatureManager.kt | 4 +- 8 files changed, 155 insertions(+), 140 deletions(-) create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/MixerStories.kt delete mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/Stories.kt diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 91558fe35..82902718a 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -296,9 +296,9 @@ "name": "Hide Quick Add in Friend Feed", "description": "Hides the Quick Add section in the friend feed" }, - "hide_story_sections": { - "name": "Hide Story Section", - "description": "Hide certain UI Elements shown in the story section" + "hide_story_suggestions": { + "name": "Hide Story Suggestions", + "description": "Removes suggestions from the Stories page" }, "hide_ui_components": { "name": "Hide UI Components", @@ -494,9 +494,9 @@ "name": "Disable Metrics", "description": "Blocks sending specific analytic data to Snapchat" }, - "disable_public_stories": { - "name": "Disable Public Stories", - "description": "Removes every public story from the Discover page\nMay require a clean cache to work properly" + "disable_story_sections": { + "name": "Disable Story Sections", + "description": "Removes sections from the Stories page\nMay require a refresh to work properly" }, "block_ads": { "name": "Block Ads", @@ -809,12 +809,9 @@ "hide_voice_record_button": "Remove Voice Record Button", "hide_unread_chat_hint": "Remove Unread Chat Hint" }, - "hide_story_sections": { + "hide_story_suggestions": { "hide_friend_suggestions": "Hide friend suggestions", - "hide_suggested_friend_stories": "Hide suggested friend stories", - "hide_friends": "Hide friends section", - "hide_suggested": "Hide suggested section", - "hide_for_you": "Hide For You section" + "hide_suggested_friend_stories": "Hide suggested friend stories" }, "home_tab": { "map": "Map", @@ -868,6 +865,11 @@ "1_month": "1 Month", "3_months": "3 Months", "6_months": "6 Months" + }, + "disable_story_sections": { + "friends": "Friends", + "following": "Following", + "discover": "Discover" } } }, diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt index 37a0cf71f..e2ef76ca7 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt @@ -12,7 +12,7 @@ class Global : ConfigContainer() { val snapchatPlus = boolean("snapchat_plus") { requireRestart() } val disableConfirmationDialogs = multiple("disable_confirmation_dialogs", "remove_friend", "block_friend", "ignore_friend", "hide_friend", "hide_conversation", "clear_conversation") { requireRestart() } val disableMetrics = boolean("disable_metrics") { requireRestart() } - val disablePublicStories = boolean("disable_public_stories") { requireRestart(); requireCleanCache() } + val disableStorySections = multiple("disable_story_sections", "friends", "following", "discover") { requireRestart(); requireCleanCache() } val blockAds = boolean("block_ads") val spotlightCommentsUsername = boolean("spotlight_comments_username") { requireRestart() } val bypassVideoLengthRestriction = unique("bypass_video_length_restriction", "split", "single") { addNotices( diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/UserInterfaceTweaks.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/UserInterfaceTweaks.kt index e3257f5fe..db24a085a 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/UserInterfaceTweaks.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/UserInterfaceTweaks.kt @@ -34,8 +34,7 @@ class UserInterfaceTweaks : ConfigContainer() { val hideFriendFeedEntry = boolean("hide_friend_feed_entry") { requireRestart() } val hideStreakRestore = boolean("hide_streak_restore") { requireRestart() } val hideQuickAddFriendFeed = boolean("hide_quick_add_friend_feed") { requireRestart() } - val hideStorySections = multiple("hide_story_sections", - "hide_friend_suggestions", "hide_suggested_friend_stories", "hide_friends", "hide_suggested", "hide_for_you") { requireRestart() } + val hideStorySuggestions = multiple("hide_story_suggestions", "hide_friend_suggestions", "hide_suggested_friend_stories") { requireRestart() } val hideUiComponents = multiple("hide_ui_components", "hide_voice_record_button", "hide_stickers_button", diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SnapEnums.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SnapEnums.kt index 20d247f89..bdeaaf9d7 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SnapEnums.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SnapEnums.kt @@ -142,4 +142,20 @@ enum class FriendLinkType(val value: Int, val shortName: String) { return entries.firstOrNull { it.value == value } ?: MUTUAL } } +} + +enum class MixerStoryType( + val index: Int, +) { + UNKNOWN(-1), + SUBSCRIPTIONS(2), + DISCOVER(3), + FRIENDS(5), + MY_STORIES(6); + + companion object { + fun fromIndex(index: Int): MixerStoryType { + return entries.firstOrNull { it.index == index } ?: UNKNOWN + } + } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/MixerStories.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/MixerStories.kt new file mode 100644 index 000000000..c3db0d7ac --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/MixerStories.kt @@ -0,0 +1,119 @@ +package me.rhunk.snapenhance.core.features.impl + +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import me.rhunk.snapenhance.common.data.StoryData +import me.rhunk.snapenhance.common.data.MixerStoryType +import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor +import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent +import me.rhunk.snapenhance.core.features.Feature +import me.rhunk.snapenhance.core.features.FeatureLoadParams +import java.nio.ByteBuffer +import kotlin.coroutines.suspendCoroutine +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +class MixerStories : Feature("MixerStories", loadParams = FeatureLoadParams.INIT_SYNC) { + @OptIn(ExperimentalEncodingApi::class) + override fun init() { + val disableDiscoverSections by context.config.global.disableStorySections + + fun canRemoveDiscoverSection(id: Int): Boolean { + val storyType = MixerStoryType.fromIndex(id) + return (storyType == MixerStoryType.SUBSCRIPTIONS && disableDiscoverSections.contains("following")) || + (storyType == MixerStoryType.DISCOVER && disableDiscoverSections.contains("discover")) || + (storyType == MixerStoryType.FRIENDS && disableDiscoverSections.contains("friends")) + } + + context.event.subscribe(NetworkApiRequestEvent::class) { event -> + fun cancelRequest() { + runBlocking { + suspendCoroutine { + context.httpServer.ensureServerStarted()?.let { server -> + event.url = "http://127.0.0.1:${server.port}" + it.resumeWith(Result.success(Unit)) + } ?: run { + event.canceled = true + it.resumeWith(Result.success(Unit)) + } + } + } + } + + if (event.url.endsWith("readreceipt-indexer/batchuploadreadreceipts")) { + if (context.config.messaging.anonymousStoryViewing.get()) { + cancelRequest() + return@subscribe + } + if (!context.config.messaging.preventStoryRewatchIndicator.get()) return@subscribe + event.hookRequestBuffer { buffer -> + ProtoEditor(buffer).apply { + edit { + get(2).removeIf { + it.toReader().getVarInt(7, 4) == 1L + } + } + }.toByteArray() + } + return@subscribe + } + + if (event.url.endsWith("df-mixer-prod/stories") || + event.url.endsWith("df-mixer-prod/batch_stories") || + event.url.endsWith("df-mixer-prod/soma/stories") || + event.url.endsWith("df-mixer-prod/soma/batch_stories") + ) { + event.onSuccess { buffer -> + val editor = ProtoEditor(buffer ?: return@onSuccess) + editor.edit { + editEach(3) { + val sectionType = firstOrNull(10)?.toReader()?.getVarInt(1)?.toInt() ?: return@editEach + + if (sectionType == MixerStoryType.FRIENDS.index && context.config.experimental.storyLogger.get()) { + val storyMap = mutableMapOf>() + + firstOrNull(3)?.toReader()?.eachBuffer(3) { + followPath(36) { + eachBuffer(1) data@{ + val userId = getString(8, 1) ?: return@data + + storyMap.getOrPut(userId) { + mutableListOf() + }.add(StoryData( + url = getString(2, 2)?.substringBefore("?") ?: return@data, + postedAt = getVarInt(3) ?: -1L, + createdAt = getVarInt(27) ?: -1L, + key = Base64.decode(getString(2, 5) ?: return@data), + iv = Base64.decode(getString(2, 4) ?: return@data) + )) + } + } + } + + context.coroutineScope.launch { + storyMap.forEach { (userId, stories) -> + stories.forEach { story -> + runCatching { + context.bridgeClient.getMessageLogger().addStory(userId, story.url, story.postedAt, story.createdAt, story.key, story.iv) + }.onFailure { + context.log.error("Failed to log story", it) + } + } + } + } + } + + if (canRemoveDiscoverSection(sectionType)) { + remove(3) + addBuffer(3, byteArrayOf()) + } + } + } + + setArg(2, ByteBuffer.wrap(editor.toByteArray())) + } + return@subscribe + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/Stories.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/Stories.kt deleted file mode 100644 index 6d1d9cc91..000000000 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/Stories.kt +++ /dev/null @@ -1,101 +0,0 @@ -package me.rhunk.snapenhance.core.features.impl - -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import me.rhunk.snapenhance.common.data.StoryData -import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor -import me.rhunk.snapenhance.common.util.protobuf.ProtoReader -import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent -import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams -import java.nio.ByteBuffer -import kotlin.coroutines.suspendCoroutine -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi - -class Stories : Feature("Stories", loadParams = FeatureLoadParams.INIT_SYNC) { - @OptIn(ExperimentalEncodingApi::class) - override fun init() { - val disablePublicStories by context.config.global.disablePublicStories - val storyLogger by context.config.experimental.storyLogger - - context.event.subscribe(NetworkApiRequestEvent::class) { event -> - fun cancelRequest() { - runBlocking { - suspendCoroutine { - context.httpServer.ensureServerStarted()?.let { server -> - event.url = "http://127.0.0.1:${server.port}" - it.resumeWith(Result.success(Unit)) - } ?: run { - event.canceled = true - it.resumeWith(Result.success(Unit)) - } - } - } - } - - if (event.url.endsWith("readreceipt-indexer/batchuploadreadreceipts")) { - if (context.config.messaging.anonymousStoryViewing.get()) { - cancelRequest() - return@subscribe - } - if (!context.config.messaging.preventStoryRewatchIndicator.get()) return@subscribe - event.hookRequestBuffer { buffer -> - ProtoEditor(buffer).apply { - edit { - get(2).removeIf { - it.toReader().getVarInt(7, 4) == 1L - } - } - }.toByteArray() - } - return@subscribe - } - if (disablePublicStories && (event.url.endsWith("df-mixer-prod/stories") || event.url.endsWith("df-mixer-prod/batch_stories"))) { - event.onSuccess { buffer -> - val payload = ProtoEditor(buffer ?: return@onSuccess).apply { - edit(3) { remove(3) } - }.toByteArray() - setArg(2, ByteBuffer.wrap(payload)) - } - return@subscribe - } - - if (storyLogger && event.url.endsWith("df-mixer-prod/soma/batch_stories")) { - event.onSuccess { buffer -> - val stories = mutableMapOf>() - val reader = ProtoReader(buffer ?: return@onSuccess) - reader.followPath(3, 3) { - eachBuffer(3) { - followPath(36) { - eachBuffer(1) data@{ - val userId = getString(8, 1) ?: return@data - - stories.getOrPut(userId) { - mutableListOf() - }.add(StoryData( - url = getString(2, 2)?.substringBefore("?") ?: return@data, - postedAt = getVarInt(3) ?: -1L, - createdAt = getVarInt(27) ?: -1L, - key = Base64.decode(getString(2, 5) ?: return@data), - iv = Base64.decode(getString(2, 4) ?: return@data) - )) - } - } - } - } - - context.coroutineScope.launch { - stories.forEach { (userId, stories) -> - stories.forEach { story -> - context.bridgeClient.getMessageLogger().addStory(userId, story.url, story.postedAt, story.createdAt, story.key, story.iv) - } - } - } - } - - return@subscribe - } - } - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/UITweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/UITweaks.kt index 6cf578d93..0429924fa 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/UITweaks.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/UITweaks.kt @@ -42,7 +42,7 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE override fun onActivityCreate() { val blockAds by context.config.global.blockAds val hiddenElements by context.config.userInterface.hideUiComponents - val hideStorySections by context.config.userInterface.hideStorySections + val hideStorySuggestions by context.config.userInterface.hideStorySuggestions val isImmersiveCamera by context.config.camera.immersiveCameraPreview val displayMetrics = context.resources.displayMetrics @@ -77,7 +77,7 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE var friendCardFrameSize: Size? = null - context.event.subscribe(BindViewEvent::class, { hideStorySections.contains("hide_suggested_friend_stories") }) { event -> + context.event.subscribe(BindViewEvent::class, { hideStorySuggestions.contains("hide_suggested_friend_stories") }) { event -> if (event.view.id != friendCardFrame) return@subscribe val friendStoryData = event.prevModel::class.java.findFieldsToString(event.prevModel, once = true) { _, value -> @@ -105,23 +105,8 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE val viewId = event.view.id val view = event.view - if (hideStorySections.contains("hide_for_you")) { - if (viewId == getId("df_large_story", "id") || - viewId == getId("df_promoted_story", "id")) { - hideStorySection(event) - return@subscribe - } - if (viewId == getId("stories_load_progress_layout", "id")) { - event.canceled = true - } - } - - if (hideStorySections.contains("hide_friends") && viewId == getId("friend_card_frame", "id")) { - hideStorySection(event) - } - //mappings? - if (hideStorySections.contains("hide_friend_suggestions") && view.javaClass.superclass?.name?.endsWith("StackDrawLayout") == true) { + if (hideStorySuggestions.contains("hide_friend_suggestions") && view.javaClass.superclass?.name?.endsWith("StackDrawLayout") == true) { val layoutParams = view.layoutParams as? FrameLayout.LayoutParams ?: return@subscribe if (layoutParams.width == -1 && layoutParams.height == -2 && @@ -134,11 +119,6 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE } } - if (hideStorySections.contains("hide_suggested") && (viewId == getId("df_small_story", "id")) - ) { - hideStorySection(event) - } - if (blockAds && viewId == getId("df_promoted_story", "id")) { hideStorySection(event) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt index dded65f09..5d107815b 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt @@ -10,7 +10,7 @@ import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.impl.ConfigurationOverride import me.rhunk.snapenhance.core.features.impl.OperaViewerParamsOverride import me.rhunk.snapenhance.core.features.impl.ScopeSync -import me.rhunk.snapenhance.core.features.impl.Stories +import me.rhunk.snapenhance.core.features.impl.MixerStories import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.core.features.impl.downloader.ProfilePictureDownloader import me.rhunk.snapenhance.core.features.impl.experiments.* @@ -113,7 +113,7 @@ class FeatureManager( BypassScreenshotDetection::class, HalfSwipeNotifier::class, DisableConfirmationDialogs::class, - Stories::class, + MixerStories::class, DisableComposerModules::class, FideliusIndicator::class, EditTextOverride::class, From 77c5abefb5dc141d451b3e2cb2613aefc1e9e15e Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sat, 13 Jan 2024 17:47:28 +0100 Subject: [PATCH 49/53] chore: update translations (#553) Co-authored-by: rhunk <101876869+rhunk@users.noreply.github.com> Co-authored-by: auth <64337177+authorisation@users.noreply.github.com> Co-authored-by: IamNotNickerson Co-authored-by: SurrenderUnstaffedFacelessSilver Co-authored-by: IamNotNickerson Co-authored-by: r_unkkk Co-authored-by: Skav Co-authored-by: L3N0X Co-authored-by: hos3366 Co-authored-by: Set Ragas Co-authored-by: Unger Szabolcs Co-authored-by: HAMO --- common/src/main/assets/lang/ar_AA.json | 158 ++++ common/src/main/assets/lang/ar_SA.json | 16 +- common/src/main/assets/lang/da.json | 10 - common/src/main/assets/lang/de_DE.json | 51 +- common/src/main/assets/lang/fi.json | 10 - common/src/main/assets/lang/fr_FR.json | 466 ++++++++++- common/src/main/assets/lang/hu_HU.json | 33 +- common/src/main/assets/lang/ml_IN.json | 1060 ++++++++++++++++++++++++ common/src/main/assets/lang/nl.json | 4 - common/src/main/assets/lang/tr_TR.json | 15 - common/src/main/assets/lang/ur_IN.json | 23 +- 11 files changed, 1739 insertions(+), 107 deletions(-) create mode 100644 common/src/main/assets/lang/ar_AA.json create mode 100644 common/src/main/assets/lang/ml_IN.json diff --git a/common/src/main/assets/lang/ar_AA.json b/common/src/main/assets/lang/ar_AA.json new file mode 100644 index 000000000..f9a84fbbb --- /dev/null +++ b/common/src/main/assets/lang/ar_AA.json @@ -0,0 +1,158 @@ +{ + "rules": { + "properties": { + "auto_download": { + "description": "تنزيل اللقطات تلقائيًا عند مشاهدتها", + "name": "التحميل التلقائي", + "options": { + "blacklist": "استبعاد من التحميل التلقائي", + "whitelist": "التحميل التلقائي" + } + }, + "stealth": { + "description": "يمنع أي شخص من معرفة أنك فتحت اللقطات / الدردشات والمحادثات", + "name": "وضع الشبح", + "options": { + "blacklist": "استبعاد من وضع الشبح", + "whitelist": "وضع التخفي" + } + }, + "auto_save": { + "name": "حفظ تلقائي", + "description": "حفظ رسائل الدردشة عند عرضها", + "options": { + "blacklist": "استبعاد من الحفظ التلقائي", + "whitelist": "حفظ تلقائي" + } + }, + "unsaveable_messages": { + "options": { + "blacklist": "استبعاد من الرسائل غير القابلة للحفظ", + "whitelist": "رسائل غير قابلة للحفظ" + }, + "name": "رسائل غير قابلة للحفظ", + "description": "يمنع حفظ الرسائل في الدردشة من قبل أشخاص آخرين" + }, + "pin_conversation": { + "name": "دبوس المحادثة" + }, + "hide_friend_feed": { + "name": "إخفاء من موجز الأصدقاء" + }, + "e2e_encryption": { + "name": "استخدم تشفير E2E" + } + }, + "modes": { + "blacklist": "وضع القائمة السوداء", + "whitelist": "وضع القائمة البيضاء" + } + }, + "actions": { + "open_map": "اختر الموقع على الخريطة", + "refresh_mappings": "تحديث التعيينات", + "check_for_updates": "التحقق من وجود تحديثات", + "bulk_messaging_action": "عمل الرسائل الجماعية", + "clean_snapchat_cache": "تنظيف ذاكرة التخزين المؤقت سناب شات", + "clear_message_logger": "مسح سجل الرسائل", + "export_chat_messages": "تصدير رسائل الدردشة", + "export_memories": "تصدير الذكريات" + }, + "features": { + "notices": { + "ban_risk": "⚠ هذه الميزة قد تسبب الحظر او الباند على حسابك", + "internal_behavior": "⚠ قد يؤدي هذا إلى كسر السلوك الداخلي في Snapchat", + "unstable": "⚠ غير مستقر", + "require_native_hooks": "⚠ تتطلب هذه الميزة استخدام خطافات أصلية تجريبية لتعمل بشكل صحيح" + }, + "properties": { + "downloader": { + "description": "تحميل سناب شات ميديا او مقاطع الفيديو", + "properties": { + "save_folder": { + "description": "حدد الدليل الذي سيتم تنزيل جميع الوسائط إليه", + "name": "حفظ المجلد" + }, + "auto_download_sources": { + "description": "‍حدد المصادر للتنزيل منها تلقائيًا", + "name": "مصادر التنزيل التلقائي" + }, + "prevent_self_auto_download": { + "name": "منع التنزيل التلقائي الذاتي" + } + }, + "name": "التحميلات" + } + } + }, + "setup": { + "dialogs": { + "select_language": "اختر اللغة", + "select_save_folder_button": "حدد مجلد", + "save_folder": "يتطلب سناب انهانس أذونات التخزين لتحميل وحفظ وسائل الإعلام من سناب شات\nيرجى اختيار المسار الذي يجب تنزيل الوسائط إليه" + }, + "mappings": { + "generate_button": "بدا التوليد", + "generate_failure": "حدث خطأ أثناء محاولة إنشاء تعيينات ، يرجى المحاولة مرة أخرى.", + "dialog": "لدعم حيوي مجموعة واسعة من الإصدارات سناب شات, و تعيينات ضرورية لسناب انهانس لتعمل بشكل صحيح, هذا لا ينبغي أن يستغرق أكثر من 5 ثواني. انتظر...", + "generate_failure_no_snapchat": "لم يتمكن سنابنهانس من اكتشاف سناب شات ، يرجى محاولة إعادة تثبيت سناب شات او التأكد انك قمت بتثبيته", + "generate_success": "التعيينات ولدت بنجاح (تجربة ممتعه)" + }, + "permissions": { + "dialog": "للمتابعة تحتاج إلى احتواء المتطلبات التالية:", + "notification_access": "وصول الإخطار", + "battery_optimization": "تحسين البطارية", + "request_button": "الطلب", + "display_over_other_apps": "الظهور فوق التطبيقات" + } + }, + "manager": { + "routes": { + "tasks": "المهام", + "social": "الاجتماعية", + "scripts": "المخطوطات", + "home": "المنزل", + "features": "الميزات", + "home_settings": "الإعدادات", + "home_logs": "السجل" + }, + "sections": { + "home": { + "logs": { + "clear_logs_button": "مسح السجل", + "export_logs_button": "تصدير السجلات" + } + }, + "tasks": { + "no_tasks": "لا توجد مهام" + }, + "features": { + "disabled": "عاجز" + }, + "social": { + "rules_title": "القواعد", + "not_found": "غير موجود", + "streaks_title": "الستريكات", + "streaks_length_text": "الطول: {الطول}", + "streaks_expiration_short": "{ساعات}", + "streaks_expiration_text": "تنتهي في {eta}", + "participants_text": "{عدد} المشاركين", + "e2ee_title": "التشفير من طرف إلى طرف", + "reminder_button": "تعيين تذكير" + } + }, + "dialogs": { + "add_friend": { + "title": "إضافة أصدقاء او مجموعه (قروب)", + "search_hint": "بحث", + "category_groups": "مجموعات (قروبات)", + "category_friends": "اصدقاء", + "fetch_error": "فشل في جلب البيانات" + }, + "scripting_warning": { + "title": "تحذير!", + "content": "يتضمن سنابنهانس أداة البرمجة النصية ، مما يسمح بتنفيذ التعليمات البرمجية المعرفة من قبل المستخدم على جهازك. توخي الحذر الشديد وتثبيت وحدات فقط من مصادر معروفة وموثوق بها. قد تشكل الوحدات غير المصرح بها أو التي لم يتم التحقق منها مخاطر أمنية على نظامك (انتبه)" + } + } + } +} diff --git a/common/src/main/assets/lang/ar_SA.json b/common/src/main/assets/lang/ar_SA.json index 19679934f..68b3c556b 100644 --- a/common/src/main/assets/lang/ar_SA.json +++ b/common/src/main/assets/lang/ar_SA.json @@ -58,6 +58,10 @@ "fetch_error": "فشل في جلب البيانات", "category_groups": "المجموعات", "category_friends": "الأصدقاء" + }, + "scripting_warning": { + "content": "يتضمن SnapEnhance أداة برمجة نصية، مما يسمح بتنفيذ تعليمات برمجية محددة من قبل المستخدم على جهازك ، (توخ الحذر بشده) وقم فقط بتثبيت الوحدات من مصادر معروفة وموثوقة. قد تشكل الوحدات غير المصرح بها أو التي لم يتم التحقق منها مخاطر أمنية على نظامك", + "title": "تحذير" } } }, @@ -99,6 +103,14 @@ }, "pin_conversation": { "name": "تثبيت المحادثة" + }, + "unsaveable_messages": { + "name": "الرسائل غير قابلة للحفظ", + "options": { + "blacklist": "استبعاد من الرسائل غير القابلة للحفظ", + "whitelist": "رسائل غير قابلة للحفظ" + }, + "description": "يمنع حفظ الرسائل في الدردشة من قبل أشخاص آخرين" } } }, @@ -108,7 +120,9 @@ "refresh_mappings": "إعادة تحميل الأداة", "open_map": "إختيار الموقع على الخريطة", "check_for_updates": "التحقق من توفر تحديثات", - "export_chat_messages": "تصدير رسائل الدردشة" + "export_chat_messages": "تصدير رسائل الدردشة", + "export_memories": "تصدير الذكريات", + "bulk_messaging_action": "عمل الرسائل الجماعية" }, "features": { "notices": { diff --git a/common/src/main/assets/lang/da.json b/common/src/main/assets/lang/da.json index 11a65ba0d..51a2b4a11 100644 --- a/common/src/main/assets/lang/da.json +++ b/common/src/main/assets/lang/da.json @@ -253,10 +253,6 @@ "hide_streak_restore": { "description": "Skjuler knappen Gendan i vennefeedet" }, - "hide_story_sections": { - "name": "Skjul Afsnittet Historie", - "description": "Skjul visse brugergrænsefladeelementer, der vises i afsnittet historie" - }, "hide_ui_components": { "name": "Skjul UI Komponenter", "description": "Vælg hvilke brugergrænsefladekomponenter der skal skjules" @@ -576,12 +572,6 @@ "hide_stickers_button": "Fjern Klistermærker Knap", "hide_voice_record_button": "Fjern Stemmeoptagelsesknap" }, - "hide_story_sections": { - "hide_friend_suggestions": "Skjul venneforslag", - "hide_friends": "Skjul vennesektion", - "hide_suggested": "Skjul foreslået sektion", - "hide_for_you": "Skjul sektion for dig" - }, "home_tab": { "map": "Kort", "chat": "Chat", diff --git a/common/src/main/assets/lang/de_DE.json b/common/src/main/assets/lang/de_DE.json index f0552dcc2..daf7c1685 100644 --- a/common/src/main/assets/lang/de_DE.json +++ b/common/src/main/assets/lang/de_DE.json @@ -50,6 +50,9 @@ "streaks_expiration_short": "{hours}h", "streaks_expiration_text": "Läuft in {eta} ab", "reminder_button": "Erinnerung festlegen" + }, + "tasks": { + "no_tasks": "Keine Aufgaben" } }, "dialogs": { @@ -122,7 +125,8 @@ "open_map": "Standort auf der Karte wählen", "check_for_updates": "Auf Updates überprüfen", "export_chat_messages": "Chat Nachrichten Exportieren", - "bulk_messaging_action": "Massen Aktion" + "bulk_messaging_action": "Massen Aktion", + "export_memories": "Memories exportieren" }, "features": { "notices": { @@ -276,10 +280,6 @@ "name": "Flammen-Wiederherstellung verstecken", "description": "Versteckt den Wiederherstellen-Button im Freundesfeed" }, - "hide_story_sections": { - "name": "Story Abschnitt ausblenden", - "description": "Blendet bestimmte, im Story Bereich angezeigte, UI-Elemente aus" - }, "hide_ui_components": { "name": "UI-Komponenten ausblenden", "description": "Wähle aus welche UI-Elemente ausgeblendet werden sollen" @@ -445,6 +445,10 @@ "instant_delete": { "name": "Sofortiges Löschen", "description": "Entfernt den Bestätigungsdialog beim Löschen von Nachrichten" + }, + "loop_media_playback": { + "name": "Medien Wiedergabe Wiederholen", + "description": "Wiederholt Snaps & Stories beim ansehen in einer Schleife" } } }, @@ -502,10 +506,6 @@ "name": "Spotlight Kommentare Benutzername", "description": "Zeigt den Benutzernamen des Autors in Spotlight-Kommentaren an" }, - "disable_public_stories": { - "name": "Öffentliche Stories deaktivieren", - "description": "Entfernt jede öffentliche Story von der Discover-Seite\nErfordert möglicherweise einen leeren Cache, um richtig zu funktionieren" - }, "force_upload_source_quality": { "name": "Upload-Qualität erzwingen", "description": "Zwingt Snapchat dazu, Medien in der Originalqualität hochzuladen\nBitte beachten Sie, dass dadurch möglicherweise keine Metadaten aus den Medien entfernt werden" @@ -790,13 +790,6 @@ "hide_voice_record_button": "Knopf für Sprachaufzeichnung entfernen", "hide_unread_chat_hint": "Hinweis auf ungelesene Chats entfernen" }, - "hide_story_sections": { - "hide_friend_suggestions": "Freundschaftsvorschläge verstecken", - "hide_friends": "Freundesbereich ausblenden", - "hide_suggested": "Verstecke empfohlen Sektion", - "hide_for_you": "For You Abschnitt ausblenden", - "hide_suggested_friend_stories": "Stories von empfohlenen Freunden ausblenden" - }, "home_tab": { "map": "Karte", "chat": "Chat", @@ -1037,5 +1030,31 @@ "message": "Das betrifft alle ausgewählten Freunde. Diese Aktion kann nicht rückgängig gemacht werden.", "title": "Sind Sie sicher?" } + }, + "media_download_source": { + "none": "Keine", + "profile_picture": "Profilbild", + "chat_media": "Chat-Medium", + "public_story": "Öffentliche Story", + "spotlight": "Spotlight", + "pending": "Ausstehend", + "story": "Story", + "story_logger": "Story Logger", + "merged": "Zusammengeführt" + }, + "material3_strings": { + "date_input_invalid_not_allowed": "Invalides Datum", + "date_range_picker_scroll_to_previous_month": "Vorheriger Monat", + "date_input_invalid_for_pattern": "Invalides Datum", + "date_picker_today_description": "Heute", + "date_picker_switch_to_calendar_mode": "Kalender", + "date_range_picker_start_headline": "Von", + "date_range_picker_end_headline": "Bis", + "date_range_picker_scroll_to_next_month": "Nächster Monat", + "date_input_invalid_year_range": "Invalides Jahr", + "date_range_input_invalid_range_input": "Ungültiger Zeitraum", + "date_picker_switch_to_input_mode": "Eingabe", + "date_range_picker_day_in_range": "Gewählt", + "date_range_picker_title": "Wähle einen Zeitraum" } } diff --git a/common/src/main/assets/lang/fi.json b/common/src/main/assets/lang/fi.json index 9ae6f83d7..6ec2929be 100644 --- a/common/src/main/assets/lang/fi.json +++ b/common/src/main/assets/lang/fi.json @@ -254,10 +254,6 @@ "name": "Piilota streakkien palautus", "description": "Piilottaa Palauta-painikkeen ystäväsyötteessä" }, - "hide_story_sections": { - "name": "Piilota tarinaosio", - "description": "Piilota tietyt tarinaosiossa näkyvät käyttöliittymäelementit" - }, "hide_ui_components": { "name": "Piilota käyttöliittymäkomponentit", "description": "Valitse piilotettavat käyttöliittymäkomponentit" @@ -578,12 +574,6 @@ "hide_stickers_button": "Poista Tarrat -painike", "hide_voice_record_button": "Poista Äänityspainike" }, - "hide_story_sections": { - "hide_friend_suggestions": "Piilota ystäväehdotukset", - "hide_friends": "Piilota ystävät-osio", - "hide_suggested": "Piilota ehdotetut osio", - "hide_for_you": "Piilota sinulle -osio" - }, "home_tab": { "map": "Kartta", "chat": "Chatti", diff --git a/common/src/main/assets/lang/fr_FR.json b/common/src/main/assets/lang/fr_FR.json index ec3e19436..3d79f16db 100644 --- a/common/src/main/assets/lang/fr_FR.json +++ b/common/src/main/assets/lang/fr_FR.json @@ -2,7 +2,8 @@ "setup": { "dialogs": { "select_language": "Choisir la langue", - "select_save_folder_button": "Sélectionner un dossier" + "select_save_folder_button": "Sélectionner un dossier", + "save_folder": "SnapEnhance requiert des autorisations de stockage pour télécharger et enregistrer des médias depuis Snapchat.\nVeuillez choisir l'emplacement où les médias doivent être téléchargés." }, "mappings": { "dialog": "Pour prendre en charge dynamiquement une large étendue de versions de Snapchat, les mappings sont nécessaires pour que SnapEnhance fonctionne correctement, cela ne devrait pas prendre plus de 5 secondes.", @@ -25,8 +26,9 @@ "home": "Accueil", "home_settings": "Paramètres", "home_logs": "Logs", - "social": "Réseaux sociaux", - "scripts": "Scripts" + "social": "Social", + "scripts": "Scripts", + "tasks": "Tâches" }, "sections": { "home": { @@ -48,6 +50,9 @@ "streaks_expiration_short": "{hours}h", "streaks_expiration_text": "Expire dans {eta}", "reminder_button": "Définir le rappel" + }, + "tasks": { + "no_tasks": "Aucune tâche" } }, "dialogs": { @@ -57,6 +62,10 @@ "fetch_error": "Échec de la récupération des données", "category_groups": "Groupes", "category_friends": "Amis" + }, + "scripting_warning": { + "content": "SnapEnhance inclut un outil de développement de scripts, permettant l'exécution d'un code défini par l'utilisateur sur votre appareil. Soyez extrêmement prudent et n'installez que des modules provenant de sources connues et fiables. Les modules non autorisés ou non vérifiés peuvent présenter des risques pour la sécurité de votre système.", + "title": "Attention" } } }, @@ -94,10 +103,18 @@ "name": "Masquer du flux d'amis" }, "e2e_encryption": { - "name": "Utilisatrice le chiffrement E2E" + "name": "Utiliser le chiffrement de bout en bout" }, "pin_conversation": { "name": "Épingler la conversation" + }, + "unsaveable_messages": { + "name": "Messages non enregistrables", + "options": { + "blacklist": "Exclure des Messages non enregistrables", + "whitelist": "Messages non enregistrables" + }, + "description": "Empêche les messages d'être enregistrés dans le chat par d'autres personnes" } } }, @@ -107,7 +124,9 @@ "refresh_mappings": "Actualiser les mappages", "open_map": "Choisir un emplacement sur la carte", "check_for_updates": "Vérifier les mises à jour", - "export_chat_messages": "Exporter les messages du chat" + "export_chat_messages": "Exporter les messages du chat", + "export_memories": "Exporter les Souvenirs", + "bulk_messaging_action": "Action de messagerie en masse" }, "features": { "notices": { @@ -198,6 +217,14 @@ "logging": { "name": "Enregistrement", "description": "Afficher une bulle de notification lorsque le média est en téléchargement" + }, + "custom_path_format": { + "description": "Spécifier un format de chemin personnalisé pour les médias téléchargés\n\nVariables disponibles :\n - %username%\n - %source%\n - %hash%\n - %date_time%", + "name": "Format de chemin personnalisé" + }, + "opera_download_button": { + "description": "Ajoute un bouton de téléchargement dans le coin en haut à droite lors de la visualisation d'un Snap", + "name": "Bouton de téléchargement Opera" } } }, @@ -249,10 +276,6 @@ "name": "Masque la restauration de Snapflamme", "description": "Masque le bouton de restauration" }, - "hide_story_sections": { - "name": "Masque la section Stories", - "description": "Masquer certains éléments visuels affichés dans la section des stories" - }, "hide_ui_components": { "name": "Masque les composants de l'interface utilisateur", "description": "Sélectionner quels éléments de l'interface est à masqué" @@ -270,7 +293,48 @@ "description": "La position des boutons du menu du fil des amis" }, "enable_friend_feed_menu_bar": { - "description": "Active la nouvelle barre de menu de flux d'amis" + "description": "Active la nouvelle barre de menu de flux d'amis", + "name": "Barre du menu du fil d'amis" + }, + "fidelius_indicator": { + "name": "Indicateur Fidelius", + "description": "Ajoute un cercle vert à côté des messages qui n'ont été envoyés qu'à vous" + }, + "hide_settings_gear": { + "name": "Masquer la roue des paramètres", + "description": "Masque la roue des paramètres SnapEnhance dans le fil d'amis" + }, + "opera_media_quick_info": { + "description": "Affiche des informations utiles sur les médias, telles que la date de création, dans le menu contextuel de la visionneuse d'Opera", + "name": "Informations rapides des médias Opera" + }, + "vertical_story_viewer": { + "name": "Visionneuse verticale de story", + "description": "Active la visualisation verticale pour toutes les Stories" + }, + "old_bitmoji_selfie": { + "name": "Ancien Selfie Bitmoji", + "description": "Restaurer les selfies Bitmoji des anciennes versions de Snapchat" + }, + "hide_quick_add_friend_feed": { + "description": "Masque la section Ajout rapide dans le fil d'amis", + "name": "Masquer l'ajout rapide dans le fil d'amis" + }, + "prevent_message_list_auto_scroll": { + "name": "Empêcher le défilement automatique de la liste des messages", + "description": "Empêche le défilement de la liste des messages vers le bas lors de l'envoi ou de la réception d'un message" + }, + "edit_text_override": { + "name": "Modification de l'éditeur de texte", + "description": "Remplace le comportement des champs de texte" + }, + "snap_preview": { + "name": "Aperçu des Snaps", + "description": "Affiche un petit aperçu à côté des Snaps non vus dans le chat" + }, + "hide_friend_feed_entry": { + "description": "Masque un ami spécifique du fil d'actualité des amis\nUtilisez l'onglet social pour gérer cette fonctionnalité", + "name": "Masquer l'entrée dans le fil d'amis" } } }, @@ -316,7 +380,21 @@ }, "message_logger": { "name": "Journalisation des messages", - "description": "Empêcher l'effacement des messages" + "description": "Empêcher l'effacement des messages", + "properties": { + "message_filter": { + "name": "Filtre de message", + "description": "Sélectionner les messages qui doivent être enregistrés (vide pour l'ensemble des messages)" + }, + "auto_purge": { + "description": "Supprime automatiquement les messages mis en cache qui sont plus anciens que la durée spécifiée", + "name": "Purge automatique" + }, + "keep_my_own_messages": { + "name": "Conserver mes propres messages", + "description": "Empêche la suppression de vos propres messages" + } + } }, "auto_save_messages_in_conversations": { "name": "Enregistrement automatique des messages", @@ -325,6 +403,52 @@ "gallery_media_send_override": { "name": "Remplacement de l'envoi des médias de la galerie", "description": "Falsifie la source du média lors de l'envoi depuis la Galerie" + }, + "loop_media_playback": { + "name": "Lecture en boucle des médias", + "description": "Lecture des médias en boucle lors de la visualisation des Snaps / Stories" + }, + "call_start_confirmation": { + "name": "Confirmation de début d'appel", + "description": "Affiche une fenêtre de confirmation lors du lancement d'un appel" + }, + "half_swipe_notifier": { + "properties": { + "min_duration": { + "name": "Durée minimale", + "description": "Durée minimale du balayage (en secondes)" + }, + "max_duration": { + "description": "Durée maximale du balayage (en secondes)", + "name": "Durée maximale" + } + }, + "name": "Notifications des entrouverts", + "description": "Vous avertit lorsque quelqu'un entrouvre une conversation" + }, + "bypass_screenshot_detection": { + "description": "Empêche Snapchat de détecter les captures d'écran", + "name": "Contourner la détection de capture d'écran" + }, + "prevent_story_rewatch_indicator": { + "name": "Empêcher l'indicateur de relecture des stories", + "description": "Empêche quiconque de savoir que vous avez revu leur Story" + }, + "instant_delete": { + "name": "Suppression instantanée", + "description": "Supprime la fenêtre de confirmation lors de la suppression de messages" + }, + "hide_peek_a_peek": { + "description": "Empêche l'envoi d'une notification lorsque vous entre ouvrez une conversation", + "name": "Cacher les ouvertures partielles des conversations" + }, + "strip_media_metadata": { + "description": "Supprime les métadonnées des médias avant de les envoyer sous forme de message", + "name": "Enlever les métadonnées des médias" + }, + "bypass_message_retention_policy": { + "name": "Contourner la politique de conservation des messages", + "description": "Empêche la disparition des messages après les avoir consultés" } } }, @@ -369,6 +493,22 @@ "disable_snap_splitting": { "name": "Désactiver le fractionnement des Snaps", "description": "Empêche les Snaps d'être divisés en plusieurs parties\nLes images que vous envoyez seront transformées en vidéos" + }, + "disable_confirmation_dialogs": { + "name": "Désactiver les fenêtres de confirmation", + "description": "Confirme automatiquement les actions sélectionnées" + }, + "suspend_location_updates": { + "name": "Suspendre les mises à jour de localisation", + "description": "Ajout d'un bouton dans les paramètres de la Snap Map pour suspendre les mises à jour de la localisation" + }, + "spotlight_comments_username": { + "name": "Nom d'utilisateur des commentaires Spotlight", + "description": "Affiche le nom d'utilisateur de l'auteur dans les commentaires Spotlight" + }, + "force_upload_source_quality": { + "name": "Forcer l'upload de la qualité originale", + "description": "Oblige Snapchat à envoyer les médias dans leur qualité d'origine\nVeuillez noter que cette opération ne supprime pas nécessairement les métadonnées des médias" } } }, @@ -403,12 +543,28 @@ "force_camera_source_encoding": { "name": "Forcer l'encodage source de la caméra", "description": "Forcer l'encodage source de la caméra" + }, + "black_photos": { + "description": "Remplace les photos capturées par un arrière-plan noir\nLes vidéos ne sont pas affectées", + "name": "Photos noires" + }, + "custom_preview_resolution": { + "description": "Définit une résolution personnalisée pour l'aperçu de la caméra, largeur x hauteur (ex : 1920x1080).\nLa résolution personnalisée doit être prise en charge par votre appareil", + "name": "Résolution de prévisualisation personnalisée" + }, + "hevc_recording": { + "name": "Enregistrement HEVC", + "description": "Utilise le codec HEVC (H.265) pour l'enregistrement vidéo" + }, + "custom_picture_resolution": { + "description": "Définit une résolution d'image personnalisée, largeur x hauteur (ex : 1920x1080).\nLa résolution personnalisée doit être prise en charge par votre appareil", + "name": "Résolution d'image personnalisée" } } }, "streaks_reminder": { - "name": "Rappels de série", - "description": "Vous notifie périodiquement de vos séries", + "name": "Rappels des Flammes", + "description": "Vous informe périodiquement concernant vos flammes", "properties": { "interval": { "name": "Fréquence", @@ -435,18 +591,46 @@ "name": "Désactiver le Bitmoji", "description": "Désactive le Bitmoji sur le profil d'amis" } - } + }, + "description": "Fonctionnalités non fiables qui se greffent au code natif de Snapchat" }, "spoof": { "name": "Falsification", - "description": "Falsifier diverses informations vous concernant" + "description": "Falsifier diverses informations vous concernant", + "properties": { + "randomize_persistent_device_token": { + "description": "Génère un jeton aléatoire après chaque connexion", + "name": "Randomiser le jeton persistant de l'appareil" + }, + "remove_mock_location_flag": { + "name": "Supprimer le flag de Mock Location", + "description": "Empêche Snapchat de détecter les localisations Mock" + }, + "android_id": { + "name": "ID Android", + "description": "Modifie l'identifiant Android avec la valeur spécifiée" + }, + "fingerprint": { + "description": "Change la fingerprint de votre appareil", + "name": "Fingerprint de l'appareil" + }, + "remove_vpn_transport_flag": { + "description": "Empêche Snapchat de détecter les VPN", + "name": "Supprimer le flag VPN Transport" + }, + "play_store_installer_package_name": { + "description": "Remplace le nom du package d'installation par com.android.vending", + "name": "Nom du package de l'installateur Play Store" + } + } }, "app_passcode": { "name": "Code d'accès de l'application", "description": "Définit un mot de passe pour verrouiller l'application" }, "app_lock_on_resume": { - "description": "Verrouille l'application lorsqu'elle est réouverte" + "description": "Verrouille l'application lorsqu'elle est réouverte", + "name": "Verrouillage de l'application à la reprise" }, "infinite_story_boost": { "name": "Booster de story infini", @@ -475,11 +659,32 @@ } }, "add_friend_source_spoof": { - "description": "Falsifie la source d'une demande d'ami" + "description": "Falsifie la source d'une demande d'ami", + "name": "Changer la source des demandes d'amis" }, "hidden_snapchat_plus_features": { "name": "Fonctionnalités Snapchat Plus cachées", "description": "Active les fonctionnalités non publiées/beta de Snapchat Plus\nPeut ne pas fonctionner sur les anciennes versions de Snapchat" + }, + "disable_composer_modules": { + "name": "Désactiver des modules Composer", + "description": "Empêche le chargement des modules composer sélectionnés\nLes noms doivent être séparés par une virgule" + }, + "meo_passcode_bypass": { + "description": "Contourner le code d'accès My Eyes Only\nCela ne fonctionnera que si le code d'accès a été saisi correctement auparavant", + "name": "Contournement du code d'accès de My Eyes Only" + }, + "prevent_forced_logout": { + "name": "Empêcher la déconnexion forcée", + "description": "Empêche Snapchat de vous déconnecter lorsque vous vous connectez sur un autre appareil" + }, + "convert_message_locally": { + "description": "Convertit localement les Snaps en médias externes dans le chat. Ceci apparaît dans le menu contextuel de téléchargement du chat", + "name": "Convertir le message localement" + }, + "story_logger": { + "description": "Fournit un historique des Stories d'amis", + "name": "Journal des Stories" } } }, @@ -494,6 +699,18 @@ "module_folder": { "name": "Dossier des modules", "description": "Le dossier où se trouvent les scripts" + }, + "integrated_ui": { + "name": "Interface utilisateur intégrée", + "description": "Permet aux scripts d'ajouter des composants d'interface utilisateur personnalisés à Snapchat" + }, + "disable_log_anonymization": { + "description": "Désactive l'anonymisation des journaux", + "name": "Désactiver l'anonymisation des journaux" + }, + "auto_reload": { + "description": "Recharge automatiquement les scripts lorsqu'ils sont modifiés", + "name": "Rechargement automatique" } } } @@ -506,14 +723,21 @@ "better_notifications": { "reply_button": "Ajouter un bouton de réponse", "download_button": "Ajouter un boutton téléchargement", - "group": "Notifications de groupe" + "group": "Notifications de groupe", + "mark_as_read_and_save_in_chat": "Sauvegarde dans le Chat lors du marquage comme lu (en fonction de la sauvegarde automatique)", + "mark_as_read_button": "Bouton \"Marquer comme lu\"", + "media_preview": "Afficher un aperçu des média", + "chat_preview": "Afficher un aperçu des messages textuels" }, "friend_feed_menu_buttons": { "auto_download": "⬇️ Téléchargement automatique", "auto_save": "💬 Enregistrement automatique des messages", "stealth": "👻 Mode incognito", "conversation_info": "👤 Infos de la Conversation", - "e2e_encryption": "🔒 Utiliser le chiffrement de bout en bout" + "e2e_encryption": "🔒 Utiliser le chiffrement de bout en bout", + "mark_stories_as_seen_locally": "👀 Marquer localement les Stories comme vues", + "mark_snaps_as_seen": "👀 Marquer les snaps comme vu", + "unsaveable_messages": "⬇️ Messages non enregistrables" }, "path_format": { "create_author_folder": "Créer un dossier pour chaque utilisateur", @@ -553,7 +777,78 @@ "abandon_video": "Appel vidéo manqué" }, "gallery_media_send_override": { - "ORIGINAL": "Originale" + "ORIGINAL": "Originale", + "NOTE": "Vocal", + "SAVABLE_SNAP": "Snap Enregistrable", + "SNAP": "Snap" + }, + "auto_reload": { + "snapchat_only": "Snapchat uniquement", + "all": "Tous (Snapchat + SnapEnhance)" + }, + "strip_media_metadata": { + "remove_audio_note_duration": "Supprimer la durée des vocaux", + "remove_audio_note_transcript_capability": "Supprimer la capacité de transcription des vocaux", + "hide_extras": "Cacher les autres informations (ex : les mentions)", + "hide_caption_text": "Cacher le texte des légendes", + "hide_snap_filters": "Cacher les filtres des Snaps" + }, + "bypass_video_length_restriction": { + "single": "Un seul média", + "split": "Division des médias" + }, + "auto_purge": { + "1_day": "1 jour", + "1_week": "1 semaine", + "1_month": "1 mois", + "2_weeks": "2 semaines", + "never": "Jamais", + "1_hour": "1 heure", + "3_hours": "3 heures", + "6_months": "6 mois", + "3_days": "3 jours", + "6_hours": "6 heures", + "3_months": "3 mois", + "12_hours": "12 heures" + }, + "home_tab": { + "map": "Carte", + "discover": "Découverte", + "spotlight": "Spotlight", + "camera": "Caméra", + "chat": "Chat" + }, + "add_friend_source_spoof": { + "added_by_community": "Par communauté", + "added_by_mention": "Par mention", + "added_by_group_chat": "Par groupe de discussion", + "added_by_username": "Par nom d'utilisateur", + "added_by_quick_add": "Par ajout rapide", + "added_by_qr_code": "Par QR Code" + }, + "disable_confirmation_dialogs": { + "hide_conversation": "Cacher la conversation", + "clear_conversation": "Effacer la conversation du fil d'amis", + "remove_friend": "Supprimer l'ami", + "hide_friend": "Cacher l'ami", + "ignore_friend": "Ignorer l'ami", + "block_friend": "Bloquer l'ami" + }, + "hide_ui_components": { + "hide_voice_record_button": "Supprimer le bouton d'enregistrement vocal", + "hide_unread_chat_hint": "Supprime l'indice de Chat non lu", + "hide_stickers_button": "Supprime le bouton des Stickers", + "hide_chat_call_buttons": "Supprimer les boutons d'appel dans le chat", + "hide_live_location_share_button": "Supprimer le bouton de partage de la localisation en direct", + "hide_profile_call_buttons": "Supprimer les boutons d'appel sur le profil" + }, + "edit_text_override": { + "bypass_text_input_limit": "Contourner la limite de saisie de texte", + "multi_line_chat_input": "Saisie multi-lignes" + }, + "old_bitmoji_selfie": { + "2d": "Bitmoji 2D", + "3d": "Bitmoji 3D" } } }, @@ -561,15 +856,24 @@ "preview": "Aperçu", "stealth_mode": "Mode furtif", "auto_download_blacklist": "Liste noire des téléchargements automatiques", - "anti_auto_save": "Empêcher l'enregistrement automatique" + "anti_auto_save": "Empêcher l'enregistrement automatique", + "mark_snaps_as_seen": "Marquer les Snaps comme vu", + "mark_stories_as_seen_locally": "Marquer localement les Stories comme vues" }, "chat_action_menu": { "preview_button": "Aperçu", "download_button": "Télécharger", - "delete_logged_message_button": "Supprimer le message enregistré" + "delete_logged_message_button": "Supprimer le message enregistré", + "convert_message": "Convertir le message" }, "opera_context_menu": { - "download": "Télécharger" + "download": "Télécharger", + "media_duration": "Durée du média : {duration} ms", + "show_debug_info": "Afficher les informations de débogage", + "expires_at": "Expire le {date}", + "created_at": "Créé le {date}", + "sent_at": "Envoyé le {date}", + "media_size": "Taille du média : {size}" }, "modal_option": { "profile_info": "Informations du profil", @@ -588,7 +892,17 @@ "title": "Informations du profil", "display_name": "Nom d'affichage", "added_date": "Date d'ajout", - "birthday": "Anniversaire : {day} {month}" + "birthday": "Anniversaire : {day} {month}", + "snapchat_plus_state": { + "subscribed": "Abonné", + "not_subscribed": "Non abonné" + }, + "friendship": "Amitié", + "hidden_birthday": "Anniversaire : Caché", + "snapchat_plus": "Snapchat Plus", + "add_source": "Source d'ajout", + "first_created_username": "Premier nom d'utilisateur créé", + "mutable_username": "Nom d'utilisateur modifiable" }, "chat_export": { "dialog_negative_button": "Annuler", @@ -600,7 +914,16 @@ "writing_output": "Écriture...", "finished": "Terminé ! Vous pouvez maintenant fermer cette fenêtre.", "no_messages_found": "Aucun message trouvé !", - "exporting_message": "Exportation de {conversation}..." + "exporting_message": "Exportation de {conversation}...", + "exporter_dialog": { + "text_field_selection_all": "Tout", + "export_file_format_title": "Format du fichier exporté", + "download_medias_title": "Télécharger les médias", + "amount_of_messages_title": "Nombre de messages (laissez vide pour tous les messages)", + "message_type_filter_title": "Filtrer les messages par type", + "text_field_selection": "{amount} sélectionné(s)", + "select_conversations_title": "Sélectionner les conversations" + } }, "button": { "ok": "Ok", @@ -641,6 +964,97 @@ }, "streaks_reminder": { "notification_title": "Rappels des flammes", - "notification_text": "Vous perdrez votre série avec {friend} dans {hoursLeft} heures" + "notification_text": "Vous perdrez vos flammes avec {friend} dans {hoursLeft} heure(s)" + }, + "content_type": { + "FAMILY_CENTER_INVITE": "Invitation Family Center", + "STATUS_CONVERSATION_CAPTURE_RECORD": "Enregistrement d'écran", + "STATUS_CALL_MISSED_VIDEO": "Appel vidéo manqué", + "CREATIVE_TOOL_ITEM": "Item de Creative Tool", + "STICKER": "Autocollant", + "TINY_SNAP": "Mini Snap", + "STATUS_SAVE_TO_CAMERA_ROLL": "Sauvegardé dans la galerie", + "EXTERNAL_MEDIA": "Média externe", + "SNAP": "Snap", + "LOCATION": "Localisation", + "CHAT": "Message Textuel", + "STATUS_PLUS_GIFT": "Cadeau Snapchat Plus", + "STATUS_COUNTDOWN": "Compte à rebours", + "LIVE_LOCATION_SHARE": "Partage d'emplacement en direct", + "STATUS": "Statut", + "STATUS_CONVERSATION_CAPTURE_SCREENSHOT": "Capture d'écran", + "FAMILY_CENTER_ACCEPT": "Acceptation Family Center", + "FAMILY_CENTER_LEAVE": "A quitté Family Center", + "STATUS_CALL_MISSED_AUDIO": "Appel audio manqué", + "NOTE": "Vocal" + }, + "media_download_source": { + "public_story": "Story Publique", + "spotlight": "Spotlight", + "pending": "En cours", + "merged": "Fusionné", + "story_logger": "Journal des stories", + "none": "Aucun", + "profile_picture": "Photo de profil", + "story": "Story", + "chat_media": "Média de Chat" + }, + "suspend_location_updates": { + "switch_text": "Suspendre les mises à jour de localisation" + }, + "material3_strings": { + "date_input_invalid_not_allowed": "Date invalide", + "date_range_input_invalid_range_input": "Plage de dates non valide", + "date_range_picker_scroll_to_previous_month": "Mois précédent", + "date_picker_switch_to_input_mode": "Entrée", + "date_range_picker_day_in_range": "Séléctionné", + "date_input_invalid_for_pattern": "Date invalide", + "date_picker_today_description": "Aujourd'hui", + "date_picker_switch_to_calendar_mode": "Calendrier", + "date_range_picker_start_headline": "Depuis", + "date_range_picker_end_headline": "Jusqu'à", + "date_range_picker_scroll_to_next_month": "Mois suivant", + "date_range_picker_title": "Sélectionner une plage de dates", + "date_input_invalid_year_range": "Année invalide" + }, + "better_notifications": { + "button": { + "download": "Télécharger", + "reply": "Répondre", + "mark_as_read": "Marquer comme lu" + }, + "stealth_mode_notice": "Impossible de marquer comme lu en mode furtif" + }, + "half_swipe_notifier": { + "notification_content_group": "{friend} a entrouvert dans {group} pendant {duration} seconde(s)", + "notification_channel_name": "Entrouverts", + "notification_content_dm": "{friend} a entrouvert votre discussion pendant {duration} seconde(s)" + }, + "friendship_link_type": { + "mutual": "Mutuel", + "deleted": "Supprimé", + "following": "Suivi", + "incoming_follower": "Follower entrant", + "incoming": "Entrant", + "blocked": "Bloqué", + "suggested": "Suggéré", + "outgoing": "Sortant" + }, + "call_start_confirmation": { + "dialog_message": "Êtes-vous sûr de vouloir lancer un appel ?", + "dialog_title": "Démarrer un appel" + }, + "bulk_messaging_action": { + "choose_action_title": "Choisir une action", + "progress_status": "Traitement de {index} sur {total}", + "actions": { + "clear_conversations": "Nettoyer des conversations", + "remove_friends": "Supprimer des amis" + }, + "selection_dialog_continue_button": "Continuer", + "confirmation_dialog": { + "message": "Cette action affectera tous les amis sélectionnés. Cette action ne peut pas être annulée.", + "title": "Êtes-vous sûr ?" + } } } diff --git a/common/src/main/assets/lang/hu_HU.json b/common/src/main/assets/lang/hu_HU.json index 0da71f938..9f69de3ff 100644 --- a/common/src/main/assets/lang/hu_HU.json +++ b/common/src/main/assets/lang/hu_HU.json @@ -27,7 +27,8 @@ "home_settings": "Beállítások", "home_logs": "Naplók", "social": "Közösség", - "scripts": "Szkriptek" + "scripts": "Szkriptek", + "tasks": "Feladatok" }, "sections": { "home": { @@ -49,6 +50,9 @@ "streaks_expiration_short": "{hours} óra", "streaks_expiration_text": "{eta} múlva jár le", "reminder_button": "Emlékeztető beállítása" + }, + "tasks": { + "no_tasks": "Nincsenek feladatok" } }, "dialogs": { @@ -58,6 +62,10 @@ "fetch_error": "Sikertelen adatlekérdezés", "category_groups": "Csoportok", "category_friends": "Barátok" + }, + "scripting_warning": { + "content": "A SnapEnhance egy script-író eszközt, amely lehetővé teszi a felhasználó által meghatározott kódok végrehajtását az eszközön. Használja fokozott óvatossággal és csak megbízható forrásból származó modulokat telepítsen. A nem engedélyezett vagy nem ellenőrzött modulok biztonsági kockázatokat jelenthetnek a rendszerének.", + "title": "Figyelmeztetés" } } }, @@ -104,7 +112,8 @@ "name": "Nem menthető üzenetek", "options": { "whitelist": "Nem menthető üzenetek" - } + }, + "description": "Megakadályozza, hogy az üzeneteket elmenthessék mások a chatben" } } }, @@ -121,7 +130,8 @@ "notices": { "unstable": "⚠ Instabil", "ban_risk": "⚠ Ez a funkció tiltást eredményezhet", - "internal_behavior": "⚠ Ez a funkció hibát okozhat a Snapchat működésében" + "internal_behavior": "⚠ Ez a funkció hibát okozhat a Snapchat működésében", + "require_native_hooks": "⚠ Ez egy kísérleti beállítás, amelyhely Native Hook-ok használata szükséges" }, "properties": { "downloader": { @@ -187,6 +197,10 @@ "custom_audio_codec": { "name": "Egyéni audió codec", "description": "Egyéni audió codec beállítása (pl.: AAC)" + }, + "threads": { + "name": "Szálak", + "description": "A felhasznált szálak mennyisége" } } }, @@ -198,7 +212,8 @@ "name": "Letöltés Operával Gomb" }, "merge_overlays": { - "description": "Egyesíti egy Snap szövegét és médiáját egy fájlba" + "description": "Egyesíti egy Snap szövegét és médiáját egy fájlba", + "name": "Overlay-ek összefűzése" } } }, @@ -245,10 +260,6 @@ "name": "Sorozat pontszám elrejtése", "description": "Elrejti a visszaállítás gombot a barát hírfolyamról" }, - "hide_story_sections": { - "name": "Történetek elrejtése", - "description": "A felhasználói felület egyes elemeinek elrejtése a történet szekcióban" - }, "hide_ui_components": { "name": "Felhasználói felület elemeinek elrejtése", "description": "Válaszd ki hogy a felhasználói felület mely részeit szeretnéd elrejteni" @@ -495,12 +506,6 @@ "hide_chat_call_buttons": "Hívás gombok eltávolítása", "hide_voice_record_button": "Hangfelvétel gomb eltávolítása" }, - "hide_story_sections": { - "hide_friend_suggestions": "Barátjavaslatok elrejtése", - "hide_friends": "Barátok szekció elrejtése", - "hide_suggested": "Javasolt szekció elrejtése", - "hide_for_you": "Neked szakasz elrejtése" - }, "home_tab": { "map": "Térkép", "chat": "Chat", diff --git a/common/src/main/assets/lang/ml_IN.json b/common/src/main/assets/lang/ml_IN.json new file mode 100644 index 000000000..14dc441e5 --- /dev/null +++ b/common/src/main/assets/lang/ml_IN.json @@ -0,0 +1,1060 @@ +{ + "rules": { + "properties": { + "stealth": { + "options": { + "blacklist": "സ്റ്റെൽത്ത് മോഡിൽ നിന്ന് ഒഴിവാക്കുക", + "whitelist": "സ്റ്റെൽത്ത് മോഡ്" + }, + "description": "നിങ്ങൾ അവരുടെ സ്നാപ്പുകൾ/ചാറ്റുകൾ, സംഭാഷണങ്ങൾ എന്നിവ തുറന്നിട്ടുണ്ടെന്ന് അറിയുന്നതിൽ നിന്ന് ആരെയും തടയുന്നു", + "name": "സ്റ്റെൽത്ത് മോഡ്" + }, + "auto_download": { + "options": { + "whitelist": "സ്വയമേവ ഡൗൺലോഡ് ചെയ്യുന്നു", + "blacklist": "ഓട്ടോ ഡൗൺലോഡിൽ നിന്ന് ഒഴിവാക്കുക" + }, + "description": "സ്നാപ്പുകൾ കാണുമ്പോൾ അവ സ്വയമേവ ഡൗൺലോഡ് ചെയ്യുക", + "name": "യാന്ത്രിക ഡൗൺലോഡ്" + }, + "auto_save": { + "name": "സ്വയമേവ സംരക്ഷിക്കുക", + "description": "ചാറ്റ് സന്ദേശങ്ങൾ കാണുമ്പോൾ അവ സംരക്ഷിക്കുന്നു", + "options": { + "blacklist": "സ്വയമേവ സംരക്ഷിക്കുന്നതിൽ നിന്ന് ഒഴിവാക്കുക", + "whitelist": "സ്വയമേവ സംരക്ഷിക്കുക" + } + }, + "e2e_encryption": { + "name": "E2E എൻക്രിപ്ഷൻ ഉപയോഗിക്കുക" + }, + "pin_conversation": { + "name": "സംഭാഷണം പിൻ ചെയ്യുക" + }, + "unsaveable_messages": { + "name": "സംരക്ഷിക്കാൻ കഴിയാത്ത സന്ദേശങ്ങൾ", + "options": { + "blacklist": "സംരക്ഷിക്കാനാകാത്ത സന്ദേശങ്ങളിൽ നിന്ന് ഒഴിവാക്കുക", + "whitelist": "സംരക്ഷിക്കാൻ കഴിയാത്ത സന്ദേശങ്ങൾ" + }, + "description": "മറ്റ് ആളുകൾ ചാറ്റിൽ സന്ദേശങ്ങൾ സംരക്ഷിക്കുന്നത് തടയുന്നു" + }, + "hide_friend_feed": { + "name": "ഫ്രണ്ട് ഫീഡിൽ നിന്ന് മറയ്ക്കുക" + } + }, + "modes": { + "blacklist": "ബ്ലാക്ക്‌ലിസ്റ്റ് മോഡ്", + "whitelist": "വൈറ്റ്‌ലിസ്റ്റ് മോഡ്" + } + }, + "manager": { + "routes": { + "home_logs": "രേഖകൾ", + "scripts": "സ്ക്രിപ്റ്റുകൾ", + "home_settings": "ക്രമീകരണങ്ങൾ", + "features": "ഫീച്ചറുകൾ", + "home": "വീട്", + "tasks": "ചുമതലകൾ", + "social": "സാമൂഹിക" + }, + "sections": { + "social": { + "rules_title": "നിയമങ്ങൾ", + "streaks_length_text": "നീളം: {length}", + "not_found": "കണ്ടെത്തിയില്ല", + "streaks_expiration_text": "ൽ കാലഹരണപ്പെടുന്നു", + "participants_text": "{count} പങ്കെടുക്കുന്നവർ", + "streaks_title": "വരകൾ", + "reminder_button": "ഓർമ്മപ്പെടുത്തൽ സജ്ജമാക്കുക", + "streaks_expiration_short": "{hours}h", + "e2ee_title": "എൻഡ്-ടു-എൻഡ് എൻക്രിപ്ഷൻ" + }, + "home": { + "logs": { + "clear_logs_button": "ലോഗുകൾ മായ്‌ക്കുക", + "export_logs_button": "ലോഗുകൾ കയറ്റുമതി ചെയ്യുക" + } + }, + "features": { + "disabled": "അപ്രാപ്തമാക്കി" + }, + "tasks": { + "no_tasks": "ജോലികളൊന്നുമില്ല" + } + }, + "dialogs": { + "scripting_warning": { + "content": "SnapEnhance-ൽ ഒരു സ്‌ക്രിപ്റ്റിംഗ് ടൂൾ ഉൾപ്പെടുന്നു, ഇത് നിങ്ങളുടെ ഉപകരണത്തിൽ ഉപയോക്തൃ-നിർവചിച്ച കോഡ് നടപ്പിലാക്കാൻ അനുവദിക്കുന്നു. അതീവ ജാഗ്രത പാലിക്കുക, അറിയപ്പെടുന്നതും വിശ്വസനീയവുമായ ഉറവിടങ്ങളിൽ നിന്നുള്ള മൊഡ്യൂളുകൾ മാത്രം ഇൻസ്റ്റാൾ ചെയ്യുക. അംഗീകൃതമല്ലാത്തതോ സ്ഥിരീകരിക്കാത്തതോ ആയ മൊഡ്യൂളുകൾ നിങ്ങളുടെ സിസ്റ്റത്തിന് സുരക്ഷാ അപകടങ്ങൾ സൃഷ്ടിച്ചേക്കാം.", + "title": "മുന്നറിയിപ്പ്" + }, + "add_friend": { + "title": "സുഹൃത്തിനെയോ ഗ്രൂപ്പിനെയോ ചേർക്കുക", + "search_hint": "തിരയുക", + "fetch_error": "ഡാറ്റ ലഭ്യമാക്കുന്നതിൽ പരാജയപ്പെട്ടു", + "category_friends": "സുഹൃത്തുക്കൾ", + "category_groups": "ഗ്രൂപ്പുകൾ" + } + } + }, + "setup": { + "mappings": { + "generate_button": "സൃഷ്ടിക്കുക", + "generate_success": "മാപ്പിംഗുകൾ വിജയകരമായി സൃഷ്ടിച്ചു.", + "dialog": "വൈവിധ്യമാർന്ന Snapchat പതിപ്പുകളെ ചലനാത്മകമായി പിന്തുണയ്ക്കുന്നതിന്, SnapEnhance ശരിയായി പ്രവർത്തിക്കുന്നതിന് മാപ്പിംഗുകൾ ആവശ്യമാണ്, ഇതിന് 5 സെക്കൻഡിൽ കൂടുതൽ എടുക്കരുത്.", + "generate_failure_no_snapchat": "SnapEnhance-ന് Snapchat കണ്ടെത്താനായില്ല, Snapchat വീണ്ടും ഇൻസ്റ്റാൾ ചെയ്യാൻ ശ്രമിക്കുക.", + "generate_failure": "മാപ്പിംഗുകൾ സൃഷ്ടിക്കാൻ ശ്രമിക്കുമ്പോൾ ഒരു പിശക് സംഭവിച്ചു, ദയവായി വീണ്ടും ശ്രമിക്കുക." + }, + "dialogs": { + "select_save_folder_button": "ഫോൾഡർ തിരഞ്ഞെടുക്കുക", + "save_folder": "സ്നാപ്ചാറ്റിൽ നിന്ന് മീഡിയ ഡൗൺലോഡ് ചെയ്യാനും സംരക്ഷിക്കാനും SnapEnhance-ന് സ്റ്റോറേജ് അനുമതികൾ ആവശ്യമാണ്\nമീഡിയ ഡൗൺലോഡ് ചെയ്യേണ്ട സ്ഥലം തിരഞ്ഞെടുക്കുക.", + "select_language": "ഭാഷ തിരഞ്ഞെടുക്കുക" + }, + "permissions": { + "battery_optimization": "ബാറ്ററി ഒപ്റ്റിമൈസേഷൻ", + "display_over_other_apps": "മറ്റ് ആപ്പുകളിൽ പ്രദർശിപ്പിക്കുക", + "notification_access": "അറിയിപ്പ് ആക്സസ്", + "dialog": "തുടരുന്നതിന് നിങ്ങൾ ഇനിപ്പറയുന്ന ആവശ്യകതകൾ പാലിക്കേണ്ടതുണ്ട്:", + "request_button": "അഭ്യർത്ഥിക്കുക" + } + }, + "material3_strings": { + "date_range_input_invalid_range_input": "അസാധുവായ തീയതി ശ്രേണി", + "date_range_picker_scroll_to_previous_month": "കഴിഞ്ഞ മാസം", + "date_picker_switch_to_input_mode": "ഇൻപുട്ട്", + "date_range_picker_day_in_range": "തിരഞ്ഞെടുത്തു", + "date_input_invalid_for_pattern": "അസാധുവായ തീയതി", + "date_picker_today_description": "ഇന്ന്", + "date_picker_switch_to_calendar_mode": "കലണ്ടർ", + "date_range_picker_end_headline": "ലേക്ക്", + "date_range_picker_scroll_to_next_month": "അടുത്ത മാസം", + "date_input_invalid_year_range": "അസാധുവായ വർഷം", + "date_range_picker_start_headline": "നിന്ന്", + "date_range_picker_title": "തീയതി ശ്രേണി തിരഞ്ഞെടുക്കുക", + "date_input_invalid_not_allowed": "അസാധുവായ തീയതി" + }, + "features": { + "properties": { + "user_interface": { + "properties": { + "friend_feed_menu_position": { + "description": "ഫ്രണ്ട് ഫീഡ് മെനു ഘടകത്തിന്റെ സ്ഥാനം", + "name": "ഫ്രണ്ട് ഫീഡ് സ്ഥാന സൂചിക" + }, + "fidelius_indicator": { + "name": "ഫിഡെലിയസ് സൂചകം", + "description": "നിങ്ങൾക്ക് മാത്രം അയച്ച സന്ദേശങ്ങൾക്ക് അടുത്തായി ഒരു പച്ച സർക്കിൾ ചേർക്കുന്നു" + }, + "streak_expiration_info": { + "description": "സ്ട്രീക്ക് കൗണ്ടറിന് അടുത്തായി ഒരു സ്ട്രീക്ക് എക്‌സ്‌പറേഷൻ ടൈമർ കാണിക്കുന്നു", + "name": "സ്ട്രീക്ക് കാലഹരണപ്പെടൽ വിവരം കാണിക്കുക" + }, + "bootstrap_override": { + "description": "ഉപയോക്തൃ ഇന്റർഫേസ് ബൂട്ട്സ്ട്രാപ്പ് ക്രമീകരണങ്ങൾ അസാധുവാക്കുന്നു", + "properties": { + "app_appearance": { + "description": "സ്ഥിരമായ ആപ്പ് രൂപഭാവം സജ്ജമാക്കുന്നു", + "name": "ആപ്പ് രൂപഭാവം" + }, + "home_tab": { + "name": "ഹോം ടാബ്", + "description": "Snapchat തുറക്കുമ്പോൾ സ്റ്റാർട്ടപ്പ് ടാബ് അസാധുവാക്കുന്നു" + } + }, + "name": "ബൂട്ട്സ്ട്രാപ്പ് അസാധുവാക്കുക" + }, + "hide_settings_gear": { + "name": "ക്രമീകരണ ഗിയർ മറയ്ക്കുക", + "description": "ഫ്രണ്ട് ഫീഡിൽ SnapEnhance Settings Gear മറയ്ക്കുന്നു" + }, + "opera_media_quick_info": { + "description": "ഓപ്പറ വ്യൂവർ സന്ദർഭ മെനുവിൽ സൃഷ്ടിച്ച തീയതി പോലുള്ള മീഡിയയുടെ ഉപയോഗപ്രദമായ വിവരങ്ങൾ കാണിക്കുന്നു", + "name": "ഓപ്പറ മീഡിയ ദ്രുത വിവരങ്ങൾ" + }, + "map_friend_nametags": { + "description": "സ്നാപ്പ്മാപ്പിലെ സുഹൃത്തുക്കളുടെ നെയിംടാഗുകൾ മെച്ചപ്പെടുത്തുന്നു", + "name": "മെച്ചപ്പെടുത്തിയ ചങ്ങാതി മാപ്പ് നെയിംടാഗുകൾ" + }, + "vertical_story_viewer": { + "name": "ലംബമായ സ്റ്റോറി വ്യൂവർ", + "description": "എല്ലാ സ്റ്റോറികൾക്കും വെർട്ടിക്കൽ സ്റ്റോറി വ്യൂവർ പ്രവർത്തനക്ഷമമാക്കുന്നു" + }, + "friend_feed_menu_buttons": { + "name": "ഫ്രണ്ട് ഫീഡ് മെനു ബട്ടണുകൾ", + "description": "ഫ്രണ്ട് ഫീഡ് മെനുവിൽ കാണിക്കേണ്ട ബട്ടണുകൾ തിരഞ്ഞെടുക്കുക" + }, + "disable_spotlight": { + "description": "സ്പോട്ട്‌ലൈറ്റ് പേജ് പ്രവർത്തനരഹിതമാക്കുന്നു", + "name": "സ്പോട്ട്ലൈറ്റ് പ്രവർത്തനരഹിതമാക്കുക" + }, + "friend_feed_message_preview": { + "name": "ഫ്രണ്ട് ഫീഡ് സന്ദേശ പ്രിവ്യൂ", + "description": "ഫ്രണ്ട് ഫീഡിലെ അവസാന സന്ദേശങ്ങളുടെ പ്രിവ്യൂ കാണിക്കുന്നു", + "properties": { + "amount": { + "name": "തുക", + "description": "പ്രിവ്യൂ ചെയ്യാനുള്ള സന്ദേശങ്ങളുടെ അളവ്" + } + } + }, + "enable_friend_feed_menu_bar": { + "description": "പുതിയ ഫ്രണ്ട് ഫീഡ് മെനു ബാർ പ്രവർത്തനക്ഷമമാക്കുന്നു", + "name": "ഫ്രണ്ട് ഫീഡ് മെനു ബാർ" + }, + "old_bitmoji_selfie": { + "name": "പഴയ ബിറ്റ്‌മോജി സെൽഫി", + "description": "പഴയ Snapchat പതിപ്പുകളിൽ നിന്ന് Bitmoji സെൽഫികൾ തിരികെ കൊണ്ടുവരുന്നു" + }, + "hide_ui_components": { + "description": "ഏത് UI ഘടകങ്ങളാണ് മറയ്ക്കേണ്ടതെന്ന് തിരഞ്ഞെടുക്കുക", + "name": "UI ഘടകങ്ങൾ മറയ്ക്കുക" + }, + "hide_quick_add_friend_feed": { + "description": "ചങ്ങാതി ഫീഡിലെ ദ്രുത ആഡ് വിഭാഗം മറയ്ക്കുന്നു", + "name": "ഫ്രണ്ട് ഫീഡിൽ ദ്രുത ആഡ് മറയ്ക്കുക" + }, + "prevent_message_list_auto_scroll": { + "name": "സന്ദേശ ലിസ്റ്റ് സ്വയമേവ സ്ക്രോൾ ചെയ്യുന്നത് തടയുക", + "description": "ഒരു സന്ദേശം അയയ്‌ക്കുമ്പോൾ/സ്വീകരിക്കുമ്പോൾ സന്ദേശ പട്ടിക താഴേക്ക് സ്ക്രോൾ ചെയ്യുന്നതിൽ നിന്ന് തടയുന്നു" + }, + "edit_text_override": { + "name": "വാചകം തിരുത്തുക", + "description": "ടെക്സ്റ്റ് ഫീൽഡ് പെരുമാറ്റം അസാധുവാക്കുന്നു" + }, + "enable_app_appearance": { + "description": "മറഞ്ഞിരിക്കുന്ന ആപ്പ് രൂപഭാവ ക്രമീകരണം പ്രവർത്തനക്ഷമമാക്കുന്നു\nപുതിയ Snapchat പതിപ്പുകളിൽ ആവശ്യമില്ലായിരിക്കാം", + "name": "ആപ്പ് രൂപഭാവ ക്രമീകരണങ്ങൾ പ്രവർത്തനക്ഷമമാക്കുക" + }, + "amoled_dark_mode": { + "description": "AMOLED ഡാർക്ക് മോഡ് പ്രവർത്തനക്ഷമമാക്കുന്നു\nSnapchats ഡാർക്ക് മോഡ് പ്രവർത്തനക്ഷമമാക്കിയിട്ടുണ്ടെന്ന് ഉറപ്പാക്കുക", + "name": "AMOLED ഡാർക്ക് മോഡ്" + }, + "snap_preview": { + "name": "സ്നാപ്പ് പ്രിവ്യൂ", + "description": "ചാറ്റിൽ കാണാത്ത സ്നാപ്പുകൾക്ക് അടുത്തായി ഒരു ചെറിയ പ്രിവ്യൂ പ്രദർശിപ്പിക്കുന്നു" + }, + "hide_friend_feed_entry": { + "description": "ഫ്രണ്ട് ഫീഡിൽ നിന്ന് ഒരു പ്രത്യേക സുഹൃത്തിനെ മറയ്ക്കുന്നു\nഈ ഫീച്ചർ മാനേജ് ചെയ്യാൻ സോഷ്യൽ ടാബ് ഉപയോഗിക്കുക", + "name": "ഫ്രണ്ട് ഫീഡ് എൻട്രി മറയ്ക്കുക" + }, + "hide_streak_restore": { + "description": "ഫ്രണ്ട് ഫീഡിലെ Restore ബട്ടൺ മറയ്ക്കുന്നു", + "name": "സ്ട്രീക്ക് വീണ്ടെടുക്കൽ മറയ്ക്കുക" + } + }, + "name": "ഉപയോക്തൃ ഇന്റർഫേസ്", + "description": "Snapchat-ന്റെ രൂപവും ഭാവവും മാറ്റുക" + }, + "camera": { + "properties": { + "custom_frame_rate": { + "description": "ക്യാമറ ഫ്രെയിം റേറ്റ് അസാധുവാക്കുന്നു", + "name": "ഇഷ്‌ടാനുസൃത ഫ്രെയിം നിരക്ക്" + }, + "custom_preview_resolution": { + "description": "ഒരു ഇഷ്‌ടാനുസൃത ക്യാമറ പ്രിവ്യൂ മിഴിവ്, വീതി x ഉയരം (ഉദാ. 1920x1080) സജ്ജമാക്കുന്നു.\nഇഷ്‌ടാനുസൃത മിഴിവ് നിങ്ങളുടെ ഉപകരണം പിന്തുണയ്ക്കണം", + "name": "ഇഷ്‌ടാനുസൃത പ്രിവ്യൂ മിഴിവ്" + }, + "override_picture_resolution": { + "description": "ചിത്ര മിഴിവ് അസാധുവാക്കുന്നു", + "name": "ചിത്ര മിഴിവ് അസാധുവാക്കുക" + }, + "hevc_recording": { + "name": "HEVC റെക്കോർഡിംഗ്", + "description": "വീഡിയോ റെക്കോർഡിംഗിനായി HEVC (H.265) കോഡെക് ഉപയോഗിക്കുന്നു" + }, + "custom_picture_resolution": { + "description": "ഒരു ഇഷ്‌ടാനുസൃത ചിത്ര മിഴിവ്, വീതി x ഉയരം (ഉദാ. 1920x1080) സജ്ജമാക്കുന്നു.\nഇഷ്‌ടാനുസൃത മിഴിവ് നിങ്ങളുടെ ഉപകരണം പിന്തുണയ്ക്കണം", + "name": "ഇഷ്‌ടാനുസൃത ചിത്ര മിഴിവ്" + }, + "disable_camera": { + "description": "നിങ്ങളുടെ ഉപകരണത്തിൽ ലഭ്യമായ ക്യാമറകൾ ഉപയോഗിക്കുന്നതിൽ നിന്ന് Snapchat തടയുന്നു", + "name": "ക്യാമറ പ്രവർത്തനരഹിതമാക്കുക" + }, + "immersive_camera_preview": { + "description": "ക്യാമറ പ്രിവ്യൂ ക്രോപ്പ് ചെയ്യുന്നതിൽ നിന്ന് Snapchat തടയുന്നു\nചില ഉപകരണങ്ങളിൽ ക്യാമറ മിന്നാൻ ഇത് കാരണമായേക്കാം", + "name": "ഇമ്മേഴ്‌സീവ് പ്രിവ്യൂ" + }, + "black_photos": { + "description": "എടുത്ത ഫോട്ടോകൾക്ക് പകരം കറുപ്പ് പശ്ചാത്തലം നൽകുന്നു\nവീഡിയോകളെ ബാധിക്കില്ല", + "name": "കറുത്ത ഫോട്ടോകൾ" + }, + "force_camera_source_encoding": { + "description": "ക്യാമറ ഉറവിട എൻകോഡിംഗിനെ നിർബന്ധിക്കുന്നു", + "name": "നിർബന്ധിത ക്യാമറ ഉറവിട എൻകോഡിംഗ്" + }, + "override_preview_resolution": { + "description": "ക്യാമറ പ്രിവ്യൂ റെസല്യൂഷൻ അസാധുവാക്കുന്നു", + "name": "പ്രിവ്യൂ റെസല്യൂഷൻ അസാധുവാക്കുക" + } + }, + "description": "മികച്ച സ്നാപ്പിനായി ശരിയായ ക്രമീകരണങ്ങൾ ക്രമീകരിക്കുക", + "name": "ക്യാമറ" + }, + "global": { + "properties": { + "spoofLocation": { + "properties": { + "coordinates": { + "description": "കോർഡിനേറ്റുകൾ സജ്ജമാക്കുക", + "name": "കോർഡിനേറ്റുകൾ" + } + }, + "description": "നിങ്ങളുടെ സ്ഥാനം കബളിപ്പിക്കുക", + "name": "സ്ഥാനം" + }, + "disable_confirmation_dialogs": { + "name": "സ്ഥിരീകരണ ഡയലോഗുകൾ പ്രവർത്തനരഹിതമാക്കുക", + "description": "തിരഞ്ഞെടുത്ത പ്രവർത്തനങ്ങൾ സ്വയമേവ സ്ഥിരീകരിക്കുന്നു" + }, + "disable_google_play_dialogs": { + "name": "Google Play സേവന ഡയലോഗുകൾ പ്രവർത്തനരഹിതമാക്കുക", + "description": "Google Play സേവനങ്ങളുടെ ലഭ്യത ഡയലോഗുകൾ കാണിക്കുന്നതിൽ നിന്ന് തടയുക" + }, + "disable_snap_splitting": { + "name": "സ്നാപ്പ് വിഭജനം പ്രവർത്തനരഹിതമാക്കുക", + "description": "Snaps ഒന്നിലധികം ഭാഗങ്ങളായി വിഭജിക്കപ്പെടുന്നത് തടയുന്നു\nനിങ്ങൾ അയയ്ക്കുന്ന ചിത്രങ്ങൾ വീഡിയോകളായി മാറും" + }, + "suspend_location_updates": { + "name": "ലൊക്കേഷൻ അപ്‌ഡേറ്റുകൾ താൽക്കാലികമായി നിർത്തുക", + "description": "ലൊക്കേഷൻ അപ്ഡേറ്റുകൾ താൽക്കാലികമായി നിർത്താൻ മാപ്പ് ക്രമീകരണങ്ങളിൽ ഒരു ബട്ടൺ ചേർക്കുന്നു" + }, + "spotlight_comments_username": { + "name": "സ്പോട്ട്ലൈറ്റ് അഭിപ്രായങ്ങളുടെ ഉപയോക്തൃനാമം", + "description": "സ്‌പോട്ട്‌ലൈറ്റ് അഭിപ്രായങ്ങളിൽ രചയിതാവിന്റെ ഉപയോക്തൃനാമം കാണിക്കുന്നു" + }, + "auto_updater": { + "description": "പുതിയ അപ്ഡേറ്റുകൾക്കായി സ്വയമേവ പരിശോധിക്കുന്നു", + "name": "യാന്ത്രിക അപ്‌ഡേറ്റർ" + }, + "disable_metrics": { + "name": "മെട്രിക്‌സ് പ്രവർത്തനരഹിതമാക്കുക", + "description": "Snapchat-ലേക്ക് നിർദ്ദിഷ്ട അനലിറ്റിക് ഡാറ്റ അയയ്ക്കുന്നത് തടയുന്നു" + }, + "bypass_video_length_restriction": { + "description": "സിംഗിൾ: ഒരൊറ്റ വീഡിയോ അയയ്ക്കുന്നു\nവിഭജിക്കുക: എഡിറ്റ് ചെയ്ത ശേഷം വീഡിയോകൾ വിഭജിക്കുക", + "name": "വീഡിയോ ദൈർഘ്യ നിയന്ത്രണങ്ങൾ മറികടക്കുക" + }, + "snapchat_plus": { + "name": "സ്നാപ്ചാറ്റ് പ്ലസ്", + "description": "Snapchat പ്ലസ് ഫീച്ചറുകൾ പ്രവർത്തനക്ഷമമാക്കുന്നു\nചില സെർവർ-വശങ്ങളുള്ള സവിശേഷതകൾ പ്രവർത്തിച്ചേക്കില്ല" + }, + "block_ads": { + "name": "പരസ്യങ്ങൾ തടയുക", + "description": "പരസ്യങ്ങൾ പ്രദർശിപ്പിക്കുന്നത് തടയുന്നു" + }, + "force_upload_source_quality": { + "name": "നിർബന്ധിത അപ്‌ലോഡ് ഉറവിട നിലവാരം", + "description": "യഥാർത്ഥ നിലവാരത്തിൽ മീഡിയ അപ്‌ലോഡ് ചെയ്യാൻ Snapchat നിർബന്ധിക്കുന്നു\nഇത് മീഡിയയിൽ നിന്ന് മെറ്റാഡാറ്റ നീക്കം ചെയ്തേക്കില്ല എന്നത് ശ്രദ്ധിക്കുക" + } + }, + "name": "ആഗോള", + "description": "ഗ്ലോബൽ സ്‌നാപ്ചാറ്റ് ക്രമീകരണങ്ങൾ മാറ്റുക" + }, + "downloader": { + "properties": { + "force_voice_note_format": { + "name": "ഫോഴ്സ് വോയ്സ് നോട്ട് ഫോർമാറ്റ്", + "description": "വോയ്‌സ് നോട്ടുകൾ ഒരു നിർദ്ദിഷ്‌ട ഫോർമാറ്റിൽ സേവ് ചെയ്യാൻ നിർബന്ധിക്കുന്നു" + }, + "chat_download_context_menu": { + "description": "ഒരു സംഭാഷണത്തിൽ നിന്ന് മീഡിയ ദീർഘനേരം അമർത്തി ഡൗൺലോഡ് ചെയ്യാൻ നിങ്ങളെ അനുവദിക്കുന്നു", + "name": "ചാറ്റ് ഡൗൺലോഡ് സന്ദർഭ മെനു" + }, + "ffmpeg_options": { + "properties": { + "constant_rate_factor": { + "description": "വീഡിയോ എൻകോഡറിനായി സ്ഥിരമായ നിരക്ക് ഘടകം സജ്ജമാക്കുക\nlibx264-ന് 0 മുതൽ 51 വരെ", + "name": "സ്ഥിരമായ നിരക്ക് ഘടകം" + }, + "custom_audio_codec": { + "name": "ഇഷ്‌ടാനുസൃത ഓഡിയോ കോഡെക്", + "description": "ഒരു ഇഷ്‌ടാനുസൃത ഓഡിയോ കോഡെക് സജ്ജീകരിക്കുക (ഉദാ. AAC)" + }, + "video_bitrate": { + "name": "വീഡിയോ ബിറ്റ്റേറ്റ്", + "description": "വീഡിയോ ബിറ്റ്റേറ്റ് (kbps) സജ്ജമാക്കുക" + }, + "threads": { + "description": "ഉപയോഗിക്കേണ്ട ത്രെഡുകളുടെ അളവ്", + "name": "ത്രെഡുകൾ" + }, + "custom_video_codec": { + "description": "ഒരു ഇഷ്‌ടാനുസൃത വീഡിയോ കോഡെക് സജ്ജീകരിക്കുക (ഉദാ. libx264)", + "name": "ഇഷ്‌ടാനുസൃത വീഡിയോ കോഡെക്" + }, + "audio_bitrate": { + "name": "ഓഡിയോ ബിറ്റ്റേറ്റ്", + "description": "ഓഡിയോ ബിറ്റ്റേറ്റ് (kbps) സജ്ജമാക്കുക" + }, + "preset": { + "name": "പ്രീസെറ്റ്", + "description": "പരിവർത്തനത്തിന്റെ വേഗത സജ്ജമാക്കുക" + } + }, + "name": "FFmpeg ഓപ്ഷനുകൾ", + "description": "അധിക FFmpeg ഓപ്ഷനുകൾ വ്യക്തമാക്കുക" + }, + "prevent_self_auto_download": { + "description": "നിങ്ങളുടെ സ്വന്തം സ്നാപ്പുകൾ സ്വയമേവ ഡൗൺലോഡ് ചെയ്യുന്നതിൽ നിന്ന് തടയുന്നു", + "name": "സ്വയം യാന്ത്രിക ഡൗൺലോഡ് തടയുക" + }, + "download_profile_pictures": { + "description": "പ്രൊഫൈൽ പേജിൽ നിന്ന് പ്രൊഫൈൽ ചിത്രങ്ങൾ ഡൗൺലോഡ് ചെയ്യാൻ നിങ്ങളെ അനുവദിക്കുന്നു", + "name": "പ്രൊഫൈൽ ചിത്രങ്ങൾ ഡൗൺലോഡ് ചെയ്യുക" + }, + "auto_download_sources": { + "description": "സ്വയമേവ ഡൗൺലോഡ് ചെയ്യാനുള്ള ഉറവിടങ്ങൾ തിരഞ്ഞെടുക്കുക", + "name": "സ്വയമേവ ഡൗൺലോഡ് ഉറവിടങ്ങൾ" + }, + "allow_duplicate": { + "name": "ഡ്യൂപ്ലിക്കേറ്റ് അനുവദിക്കുക", + "description": "ഒരേ മീഡിയ ഒന്നിലധികം തവണ ഡൗൺലോഡ് ചെയ്യാൻ അനുവദിക്കുന്നു" + }, + "custom_path_format": { + "description": "ഡൗൺലോഡ് ചെയ്‌ത മീഡിയയ്‌ക്കായി ഒരു ഇഷ്‌ടാനുസൃത പാത്ത് ഫോർമാറ്റ് വ്യക്തമാക്കുക\n\nലഭ്യമായ വേരിയബിളുകൾ:\n - %ഉപയോക്തൃനാമം%\n - %ഉറവിടം%\n - %ഹാഷ്%\n - %തീയതി സമയം%", + "name": "ഇഷ്‌ടാനുസൃത പാത്ത് ഫോർമാറ്റ്" + }, + "force_image_format": { + "name": "ഫോഴ്സ് ഇമേജ് ഫോർമാറ്റ്", + "description": "ചിത്രങ്ങൾ ഒരു നിർദ്ദിഷ്ട ഫോർമാറ്റിൽ സംരക്ഷിക്കാൻ നിർബന്ധിക്കുന്നു" + }, + "opera_download_button": { + "description": "ഒരു Snap കാണുമ്പോൾ മുകളിൽ വലത് കോണിൽ ഒരു ഡൗൺലോഡ് ബട്ടൺ ചേർക്കുന്നു", + "name": "ഓപ്പറ ഡൗൺലോഡ് ബട്ടൺ" + }, + "save_folder": { + "name": "ഫോൾഡർ സംരക്ഷിക്കുക", + "description": "എല്ലാ മീഡിയയും ഡൗൺലോഡ് ചെയ്യേണ്ട ഡയറക്‌ടറി തിരഞ്ഞെടുക്കുക" + }, + "merge_overlays": { + "name": "ഓവർലേകൾ ലയിപ്പിക്കുക", + "description": "ഒരു സ്‌നാപ്പിന്റെ ടെക്‌സ്‌റ്റും മീഡിയയും ഒരു ഫയലായി സംയോജിപ്പിക്കുന്നു" + }, + "logging": { + "description": "മീഡിയ ഡൗൺലോഡ് ചെയ്യുമ്പോൾ ടോസ്റ്റുകൾ കാണിക്കുന്നു", + "name": "ലോഗിംഗ്" + }, + "path_format": { + "name": "പാത്ത് ഫോർമാറ്റ്", + "description": "ഫയൽ പാത്ത് ഫോർമാറ്റ് വ്യക്തമാക്കുക" + } + }, + "description": "Snapchat മീഡിയ ഡൗൺലോഡ് ചെയ്യുക", + "name": "ഡൗൺലോഡർ" + }, + "experimental": { + "properties": { + "spoof": { + "properties": { + "randomize_persistent_device_token": { + "description": "ഓരോ ലോഗിൻ ശേഷവും ഒരു റാൻഡം ഉപകരണ ടോക്കൺ സൃഷ്ടിക്കുന്നു", + "name": "പെർസിസ്റ്റന്റ് ഉപകരണ ടോക്കൺ ക്രമരഹിതമാക്കുക" + }, + "remove_mock_location_flag": { + "name": "മോക്ക് ലൊക്കേഷൻ ഫ്ലാഗ് നീക്കം ചെയ്യുക", + "description": "മോക്ക് ലൊക്കേഷൻ കണ്ടെത്തുന്നതിൽ നിന്ന് Snapchat തടയുന്നു" + }, + "android_id": { + "name": "ആൻഡ്രോയിഡ് ഐഡി", + "description": "നിർദ്ദിഷ്‌ട മൂല്യത്തിലേക്ക് നിങ്ങളുടെ Android ഐഡി കബളിപ്പിക്കുന്നു" + }, + "fingerprint": { + "description": "നിങ്ങളുടെ ഉപകരണ ഫിംഗർപ്രിന്റ് കബളിപ്പിക്കുന്നു", + "name": "ഉപകരണ ഫിംഗർപ്രിന്റ്" + }, + "remove_vpn_transport_flag": { + "description": "VPN-കൾ കണ്ടെത്തുന്നതിൽ നിന്ന് Snapchat തടയുന്നു", + "name": "VPN ട്രാൻസ്പോർട്ട് ഫ്ലാഗ് നീക്കം ചെയ്യുക" + }, + "play_store_installer_package_name": { + "description": "com.android.vending എന്നതിലേക്ക് ഇൻസ്റ്റാളർ പാക്കേജിന്റെ പേര് അസാധുവാക്കുന്നു", + "name": "പ്ലേ സ്റ്റോർ ഇൻസ്റ്റാളർ പാക്കേജിന്റെ പേര്" + } + }, + "description": "നിങ്ങളെക്കുറിച്ചുള്ള വിവിധ വിവരങ്ങൾ കബളിപ്പിക്കുക", + "name": "സ്പൂഫ്" + }, + "native_hooks": { + "properties": { + "disable_bitmoji": { + "description": "ചങ്ങാതിമാരുടെ പ്രൊഫൈൽ ബിറ്റ്മോജി പ്രവർത്തനരഹിതമാക്കുന്നു", + "name": "ബിറ്റ്‌മോജി പ്രവർത്തനരഹിതമാക്കുക" + } + }, + "description": "Snapchat-ന്റെ നേറ്റീവ് കോഡിലേക്ക് ഹുക്ക് ചെയ്യുന്ന സുരക്ഷിതമല്ലാത്ത ഫീച്ചറുകൾ", + "name": "നേറ്റീവ് ഹുക്കുകൾ" + }, + "convert_message_locally": { + "description": "പ്രാദേശികമായി ബാഹ്യ മീഡിയ ചാറ്റ് ചെയ്യാൻ സ്നാപ്പുകൾ പരിവർത്തനം ചെയ്യുന്നു. ചാറ്റ് ഡൗൺലോഡ് സന്ദർഭ മെനുവിൽ ഇത് ദൃശ്യമാകും", + "name": "സന്ദേശം പ്രാദേശികമായി പരിവർത്തനം ചെയ്യുക" + }, + "story_logger": { + "name": "സ്റ്റോറി ലോഗർ", + "description": "സുഹൃത്തുക്കളുടെ കഥകളുടെ ചരിത്രം നൽകുന്നു" + }, + "app_passcode": { + "name": "ആപ്പ് പാസ്‌കോഡ്", + "description": "ആപ്പ് ലോക്ക് ചെയ്യാൻ ഒരു പാസ്‌കോഡ് സജ്ജീകരിക്കുന്നു" + }, + "app_lock_on_resume": { + "name": "ആപ്പ് ലോക്ക് ഓൺ റെസ്യൂമെ", + "description": "ആപ്പ് വീണ്ടും തുറക്കുമ്പോൾ അത് ലോക്ക് ചെയ്യുന്നു" + }, + "infinite_story_boost": { + "name": "അനന്തമായ കഥ ബൂസ്റ്റ്", + "description": "സ്റ്റോറി ബൂസ്റ്റ് പരിധി കാലതാമസം മറികടക്കുക" + }, + "unlimited_multi_snap": { + "name": "അൺലിമിറ്റഡ് മൾട്ടി സ്നാപ്പ്", + "description": "അൺലിമിറ്റഡ് തുക മൾട്ടി സ്നാപ്പുകൾ എടുക്കാൻ നിങ്ങളെ അനുവദിക്കുന്നു" + }, + "no_friend_score_delay": { + "name": "ഫ്രണ്ട് സ്‌കോർ കാലതാമസം ഇല്ല", + "description": "ഒരു ഫ്രണ്ട്സ് സ്കോർ കാണുമ്പോഴുള്ള കാലതാമസം നീക്കം ചെയ്യുന്നു" + }, + "e2ee": { + "properties": { + "encrypted_message_indicator": { + "name": "എൻക്രിപ്റ്റ് ചെയ്ത സന്ദേശ സൂചകം", + "description": "എൻക്രിപ്റ്റ് ചെയ്ത സന്ദേശങ്ങൾക്ക് അടുത്തായി ഒരു 🔒 ഇമോജി ചേർക്കുന്നു" + }, + "force_message_encryption": { + "name": "നിർബന്ധിത സന്ദേശ എൻക്രിപ്ഷൻ", + "description": "ഒന്നിലധികം സംഭാഷണങ്ങൾ തിരഞ്ഞെടുക്കുമ്പോൾ മാത്രം E2E എൻക്രിപ്ഷൻ പ്രവർത്തനക്ഷമമാക്കാത്ത ആളുകൾക്ക് എൻക്രിപ്റ്റ് ചെയ്ത സന്ദേശങ്ങൾ അയക്കുന്നത് തടയുന്നു" + } + }, + "name": "എൻഡ്-ടു-എൻഡ് എൻക്രിപ്ഷൻ", + "description": "പങ്കിട്ട രഹസ്യ കീ ഉപയോഗിച്ച് AES ഉപയോഗിച്ച് നിങ്ങളുടെ സന്ദേശങ്ങൾ എൻക്രിപ്റ്റ് ചെയ്യുന്നു\nനിങ്ങളുടെ താക്കോൽ സുരക്ഷിതമായി എവിടെയെങ്കിലും സൂക്ഷിക്കുന്നത് ഉറപ്പാക്കുക!" + }, + "add_friend_source_spoof": { + "name": "സുഹൃത്ത് ഉറവിട സ്പൂഫ് ചേർക്കുക", + "description": "ഒരു സുഹൃത്ത് അഭ്യർത്ഥനയുടെ ഉറവിടം കബളിപ്പിക്കുന്നു" + }, + "hidden_snapchat_plus_features": { + "name": "മറഞ്ഞിരിക്കുന്ന Snapchat പ്ലസ് ഫീച്ചറുകൾ", + "description": "റിലീസ് ചെയ്യാത്ത/ബീറ്റ Snapchat പ്ലസ് ഫീച്ചറുകൾ പ്രവർത്തനക്ഷമമാക്കുന്നു\nപഴയ Snapchat പതിപ്പുകളിൽ പ്രവർത്തിച്ചേക്കില്ല" + }, + "disable_composer_modules": { + "name": "കമ്പോസർ മൊഡ്യൂളുകൾ പ്രവർത്തനരഹിതമാക്കുക", + "description": "തിരഞ്ഞെടുത്ത കമ്പോസർ മൊഡ്യൂളുകൾ ലോഡ് ചെയ്യുന്നതിൽ നിന്ന് തടയുന്നു\nപേരുകൾ ഒരു കോമ കൊണ്ട് വേർതിരിക്കേണ്ടതാണ്" + }, + "prevent_forced_logout": { + "name": "നിർബന്ധിത ലോഗ്ഔട്ട് തടയുക", + "description": "നിങ്ങൾ മറ്റൊരു ഉപകരണത്തിൽ ലോഗിൻ ചെയ്യുമ്പോൾ നിങ്ങളെ ലോഗ് ഔട്ട് ചെയ്യുന്നതിൽ നിന്ന് Snapchat തടയുന്നു" + }, + "meo_passcode_bypass": { + "name": "എന്റെ കണ്ണുകൾ മാത്രം പാസ്‌കോഡ് ബൈപാസ്", + "description": "മൈ ഐസ് ഒൺലി പാസ്‌കോഡ് ബൈപാസ് ചെയ്യുക\nമുമ്പ് പാസ്‌കോഡ് ശരിയായി നൽകിയിട്ടുണ്ടെങ്കിൽ മാത്രമേ ഇത് പ്രവർത്തിക്കൂ" + } + }, + "description": "പരീക്ഷണാത്മക സവിശേഷതകൾ", + "name": "പരീക്ഷണാത്മകം" + }, + "messaging": { + "properties": { + "notification_blacklist": { + "name": "നോട്ടിഫിക്കേഷൻ ബ്ലാക്ക്‌ലിസ്റ്റ്", + "description": "ബ്ലോക്ക് ചെയ്യേണ്ട അറിയിപ്പുകൾ തിരഞ്ഞെടുക്കുക" + }, + "prevent_message_sending": { + "name": "സന്ദേശം അയയ്ക്കുന്നത് തടയുക", + "description": "ചില തരത്തിലുള്ള സന്ദേശങ്ങൾ അയക്കുന്നത് തടയുന്നു" + }, + "message_logger": { + "properties": { + "message_filter": { + "name": "സന്ദേശ ഫിൽട്ടർ", + "description": "ഏതൊക്കെ സന്ദേശങ്ങളാണ് ലോഗിൻ ചെയ്യേണ്ടതെന്ന് തിരഞ്ഞെടുക്കുക (എല്ലാ സന്ദേശങ്ങൾക്കും ശൂന്യം)" + }, + "auto_purge": { + "description": "നിർദ്ദിഷ്‌ട സമയത്തേക്കാൾ പഴയ കാഷെ ചെയ്‌ത സന്ദേശങ്ങൾ സ്വയമേവ ഇല്ലാതാക്കുന്നു", + "name": "യാന്ത്രിക ശുദ്ധീകരണം" + }, + "keep_my_own_messages": { + "name": "എന്റെ സ്വന്തം സന്ദേശങ്ങൾ സൂക്ഷിക്കുക", + "description": "നിങ്ങളുടെ സ്വന്തം സന്ദേശങ്ങൾ ഇല്ലാതാക്കുന്നതിൽ നിന്ന് തടയുന്നു" + } + }, + "description": "സന്ദേശങ്ങൾ ഇല്ലാതാക്കുന്നത് തടയുന്നു", + "name": "സന്ദേശ ലോഗർ" + }, + "anonymous_story_viewing": { + "name": "അജ്ഞാത കഥ കാണൽ", + "description": "നിങ്ങൾ അവരുടെ കഥ കണ്ടുവെന്ന് അറിയുന്നതിൽ നിന്ന് ആരെയും തടയുന്നു" + }, + "loop_media_playback": { + "name": "ലൂപ്പ് മീഡിയ പ്ലേബാക്ക്", + "description": "സ്നാപ്പുകൾ / സ്റ്റോറികൾ കാണുമ്പോൾ മീഡിയ പ്ലേബാക്ക് ലൂപ്പ് ചെയ്യുന്നു" + }, + "disable_replay_in_ff": { + "description": "ഫ്രണ്ട് ഫീഡിൽ നിന്ന് ദീർഘനേരം അമർത്തി വീണ്ടും പ്ലേ ചെയ്യാനുള്ള കഴിവ് പ്രവർത്തനരഹിതമാക്കുന്നു", + "name": "FF-ൽ റീപ്ലേ പ്രവർത്തനരഹിതമാക്കുക" + }, + "call_start_confirmation": { + "name": "കോൾ ആരംഭ സ്ഥിരീകരണം", + "description": "ഒരു കോൾ ആരംഭിക്കുമ്പോൾ ഒരു സ്ഥിരീകരണ ഡയലോഗ് കാണിക്കുന്നു" + }, + "half_swipe_notifier": { + "properties": { + "min_duration": { + "name": "കുറഞ്ഞ ദൈർഘ്യം", + "description": "പകുതി സ്വൈപ്പിന്റെ ഏറ്റവും കുറഞ്ഞ ദൈർഘ്യം (സെക്കൻഡിൽ)" + }, + "max_duration": { + "description": "പകുതി സ്വൈപ്പിന്റെ പരമാവധി ദൈർഘ്യം (സെക്കൻഡിൽ)", + "name": "പരമാവധി ദൈർഘ്യം" + } + }, + "name": "ഹാഫ് സ്വൈപ്പ് നോട്ടിഫയർ", + "description": "ആരെങ്കിലും സംഭാഷണത്തിലേക്ക് പാതി സ്വൈപ്പ് ചെയ്യുമ്പോൾ നിങ്ങളെ അറിയിക്കും" + }, + "bypass_screenshot_detection": { + "description": "നിങ്ങൾ സ്‌ക്രീൻഷോട്ട് എടുക്കുമ്പോൾ സ്‌നാപ്ചാറ്റ് കണ്ടെത്തുന്നതിൽ നിന്ന് തടയുന്നു", + "name": "ബൈപാസ് സ്ക്രീൻഷോട്ട് കണ്ടെത്തൽ" + }, + "gallery_media_send_override": { + "description": "ഗാലറിയിൽ നിന്ന് അയയ്‌ക്കുമ്പോൾ മീഡിയ ഉറവിടം കബളിപ്പിക്കുന്നു", + "name": "ഗാലറി മീഡിയ അയയ്‌ക്കുക അസാധുവാക്കുക" + }, + "message_preview_length": { + "description": "പ്രിവ്യൂ ചെയ്യാനുള്ള സന്ദേശങ്ങളുടെ അളവ് വ്യക്തമാക്കുക", + "name": "സന്ദേശ പ്രിവ്യൂ ദൈർഘ്യം" + }, + "strip_media_metadata": { + "description": "ഒരു സന്ദേശമായി അയയ്‌ക്കുന്നതിന് മുമ്പ് മീഡിയയുടെ മെറ്റാഡാറ്റ നീക്കംചെയ്യുന്നു", + "name": "സ്ട്രിപ്പ് മീഡിയ മെറ്റാഡാറ്റ" + }, + "better_notifications": { + "description": "ലഭിച്ച അറിയിപ്പുകളിൽ കൂടുതൽ വിവരങ്ങൾ ചേർക്കുന്നു", + "name": "മികച്ച അറിയിപ്പുകൾ" + }, + "auto_save_messages_in_conversations": { + "name": "സന്ദേശങ്ങൾ സ്വയമേവ സംരക്ഷിക്കുക", + "description": "സംഭാഷണങ്ങളിലെ എല്ലാ സന്ദേശങ്ങളും സ്വയമേവ സംരക്ഷിക്കുന്നു" + }, + "bypass_message_retention_policy": { + "name": "ബൈപാസ് സന്ദേശം നിലനിർത്തൽ നയം", + "description": "സന്ദേശങ്ങൾ കണ്ടതിന് ശേഷം ഇല്ലാതാക്കുന്നത് തടയുന്നു" + }, + "prevent_story_rewatch_indicator": { + "name": "സ്റ്റോറി റീവാച്ച് ഇൻഡിക്കേറ്റർ തടയുക", + "description": "നിങ്ങൾ അവരുടെ കഥ വീണ്ടും കണ്ടുവെന്ന് അറിയുന്നതിൽ നിന്ന് ആരെയും തടയുന്നു" + }, + "instant_delete": { + "name": "തൽക്ഷണ ഇല്ലാതാക്കൽ", + "description": "സന്ദേശങ്ങൾ ഇല്ലാതാക്കുമ്പോൾ സ്ഥിരീകരണ ഡയലോഗ് നീക്കംചെയ്യുന്നു" + }, + "hide_bitmoji_presence": { + "name": "ബിറ്റ്‌മോജി സാന്നിധ്യം മറയ്ക്കുക", + "description": "ചാറ്റിൽ ആയിരിക്കുമ്പോൾ നിങ്ങളുടെ ബിറ്റ്‌മോജി പോപ്പ് അപ്പ് ചെയ്യുന്നത് തടയുന്നു" + }, + "unlimited_snap_view_time": { + "name": "അൺലിമിറ്റഡ് സ്നാപ്പ് കാഴ്ച സമയം", + "description": "സ്നാപ്പുകൾ കാണുന്നതിനുള്ള സമയ പരിധി നീക്കം ചെയ്യുന്നു" + }, + "hide_typing_notifications": { + "description": "നിങ്ങൾ ഒരു സന്ദേശം ടൈപ്പുചെയ്യുന്നത് അറിയുന്നതിൽ നിന്ന് ആരെയും തടയുന്നു", + "name": "ടൈപ്പിംഗ് അറിയിപ്പുകൾ മറയ്ക്കുക" + }, + "hide_peek_a_peek": { + "description": "നിങ്ങൾ ഒരു ചാറ്റിലേക്ക് പകുതി സ്വൈപ്പ് ചെയ്യുമ്പോൾ അറിയിപ്പ് അയയ്ക്കുന്നത് തടയുന്നു", + "name": "പീക്ക്-എ-പീക്ക് മറയ്ക്കുക" + } + }, + "name": "സന്ദേശമയയ്ക്കൽ", + "description": "നിങ്ങൾ സുഹൃത്തുക്കളുമായി ഇടപഴകുന്ന രീതി മാറ്റുക" + }, + "streaks_reminder": { + "properties": { + "group_notifications": { + "description": "ഗ്രൂപ്പ് അറിയിപ്പുകൾ ഒറ്റ ഒന്നായി", + "name": "ഗ്രൂപ്പ് അറിയിപ്പുകൾ" + }, + "interval": { + "name": "ഇടവേള", + "description": "ഓരോ ഓർമ്മപ്പെടുത്തലുകൾക്കിടയിലുള്ള ഇടവേള (മണിക്കൂറുകൾ)" + }, + "remaining_hours": { + "name": "ശേഷിക്കുന്ന സമയം", + "description": "അറിയിപ്പിന് മുമ്പുള്ള ശേഷിക്കുന്ന സമയം കാണിക്കുന്നു" + } + }, + "description": "നിങ്ങളുടെ സ്ട്രീക്കുകളെക്കുറിച്ച് ആനുകാലികമായി നിങ്ങളെ അറിയിക്കുന്നു", + "name": "സ്ട്രീക്കുകൾ ഓർമ്മപ്പെടുത്തൽ" + }, + "rules": { + "description": "വ്യക്തിഗത ആളുകൾക്കായി സ്വയമേവയുള്ള സവിശേഷതകൾ നിയന്ത്രിക്കുക", + "name": "നിയമങ്ങൾ" + }, + "scripting": { + "description": "SnapEnhance വിപുലീകരിക്കാൻ ഇഷ്ടാനുസൃത സ്ക്രിപ്റ്റുകൾ പ്രവർത്തിപ്പിക്കുക", + "name": "സ്ക്രിപ്റ്റിംഗ്", + "properties": { + "developer_mode": { + "name": "ഡെവലപ്പർ മോഡ്", + "description": "Snapchat-ന്റെ UI-യിൽ ഡീബഗ് വിവരങ്ങൾ കാണിക്കുന്നു" + }, + "module_folder": { + "name": "മൊഡ്യൂൾ ഫോൾഡർ", + "description": "സ്ക്രിപ്റ്റുകൾ സ്ഥിതി ചെയ്യുന്ന ഫോൾഡർ" + }, + "auto_reload": { + "name": "യാന്ത്രികമായി വീണ്ടും ലോഡുചെയ്യുക", + "description": "സ്ക്രിപ്റ്റുകൾ മാറുമ്പോൾ അവ സ്വയമേവ റീലോഡ് ചെയ്യുന്നു" + }, + "integrated_ui": { + "name": "ഇന്റഗ്രേറ്റഡ് യുഐ", + "description": "Snapchat-ലേക്ക് ഇഷ്‌ടാനുസൃത UI ഘടകങ്ങൾ ചേർക്കാൻ സ്‌ക്രിപ്റ്റുകളെ അനുവദിക്കുന്നു" + }, + "disable_log_anonymization": { + "name": "ലോഗ് അജ്ഞാതമാക്കൽ പ്രവർത്തനരഹിതമാക്കുക", + "description": "ലോഗുകളുടെ അജ്ഞാതവൽക്കരണം പ്രവർത്തനരഹിതമാക്കുന്നു" + } + } + } + }, + "notices": { + "internal_behavior": "⚠ ഇത് Snapchat ആന്തരിക സ്വഭാവത്തെ തകർത്തേക്കാം", + "ban_risk": "⚠ ഈ സവിശേഷത വിലക്കുകൾക്ക് കാരണമായേക്കാം", + "unstable": "⚠ അസ്ഥിരമാണ്", + "require_native_hooks": "⚠ ഈ ഫീച്ചറിന് ശരിയായി പ്രവർത്തിക്കാൻ പരീക്ഷണാത്മക നേറ്റീവ് ഹുക്കുകൾ ആവശ്യമാണ്" + }, + "options": { + "better_notifications": { + "download_button": "ഡൗൺലോഡ് ബട്ടൺ ചേർക്കുക", + "mark_as_read_button": "റീഡ് ബട്ടണായി അടയാളപ്പെടുത്തുക", + "mark_as_read_and_save_in_chat": "വായിച്ചതായി അടയാളപ്പെടുത്തുമ്പോൾ ചാറ്റിൽ സംരക്ഷിക്കുക (സ്വയമേവ സംരക്ഷിക്കുന്നതിനെ ആശ്രയിച്ചിരിക്കുന്നു)", + "group": "ഗ്രൂപ്പ് അറിയിപ്പുകൾ", + "chat_preview": "ചാറ്റിന്റെ പ്രിവ്യൂ കാണിക്കുക", + "media_preview": "മീഡിയയുടെ പ്രിവ്യൂ കാണിക്കുക", + "reply_button": "മറുപടി ബട്ടൺ ചേർക്കുക" + }, + "friend_feed_menu_buttons": { + "auto_download": "⬇️ ഓട്ടോ ഡൗൺലോഡ്", + "auto_save": "💬 സന്ദേശങ്ങൾ സ്വയമേവ സംരക്ഷിക്കുക", + "unsaveable_messages": "⬇️ സംരക്ഷിക്കാനാവാത്ത സന്ദേശങ്ങൾ", + "stealth": "👻 സ്റ്റെൽത്ത് മോഡ്", + "conversation_info": "👤 സംഭാഷണ വിവരം", + "e2e_encryption": "🔒 E2E എൻക്രിപ്ഷൻ ഉപയോഗിക്കുക", + "mark_snaps_as_seen": "👀 Snaps കണ്ടതായി അടയാളപ്പെടുത്തുക", + "mark_stories_as_seen_locally": "👀 പ്രാദേശികമായി കാണുന്ന കഥകൾ അടയാളപ്പെടുത്തുക" + }, + "path_format": { + "create_author_folder": "ഓരോ രചയിതാവിനും ഫോൾഡർ സൃഷ്ടിക്കുക", + "create_source_folder": "ഓരോ മീഡിയ ഉറവിട തരത്തിനും ഫോൾഡർ സൃഷ്‌ടിക്കുക", + "append_hash": "ഫയലിന്റെ പേരിൽ ഒരു അദ്വിതീയ ഹാഷ് ചേർക്കുക", + "append_username": "ഫയലിന്റെ പേരിലേക്ക് ഉപയോക്തൃനാമം ചേർക്കുക", + "append_date_time": "ഫയലിന്റെ പേരിൽ തീയതിയും സമയവും ചേർക്കുക", + "append_source": "ഫയലിന്റെ പേരിലേക്ക് മീഡിയ ഉറവിടം ചേർക്കുക" + }, + "auto_download_sources": { + "friend_stories": "സുഹൃത്ത് കഥകൾ", + "public_stories": "പൊതു കഥകൾ", + "spotlight": "സ്പോട്ട്ലൈറ്റ്", + "friend_snaps": "സുഹൃത്ത് സ്നാപ്സ്" + }, + "logging": { + "progress": "പുരോഗതി", + "failure": "പരാജയം", + "started": "ആരംഭിച്ചു", + "success": "വിജയം" + }, + "notifications": { + "chat_screenshot": "സ്ക്രീൻഷോട്ട്", + "chat_screen_record": "സ്ക്രീൻ റെക്കോർഡ്", + "snap_replay": "സ്നാപ്പ് റീപ്ലേ", + "camera_roll_save": "ക്യാമറ റോൾ സേവ്", + "chat": "ചാറ്റ്", + "chat_reply": "ചാറ്റ് മറുപടി", + "snap": "സ്നാപ്പ്", + "typing": "ടൈപ്പിംഗ്", + "stories": "കഥകൾ", + "group_chat_reaction": "ഗ്രൂപ്പ് പ്രതികരണം", + "initiate_audio": "ഇൻകമിംഗ് ഓഡിയോ കോൾ", + "abandon_audio": "മിസ്‌ഡ് ഓഡിയോ കോൾ", + "abandon_video": "മിസ്‌ഡ് വീഡിയോ കോൾ", + "chat_reaction": "ഡിഎം പ്രതികരണം", + "initiate_video": "ഇൻകമിംഗ് വീഡിയോ കോൾ" + }, + "gallery_media_send_override": { + "ORIGINAL": "ഒറിജിനൽ", + "NOTE": "ഓഡിയോ കുറിപ്പ്", + "SAVABLE_SNAP": "സേവ് ചെയ്യാവുന്ന സ്നാപ്പ്", + "SNAP": "സ്നാപ്പ്" + }, + "strip_media_metadata": { + "hide_snap_filters": "സ്നാപ്പ് ഫിൽട്ടറുകൾ മറയ്ക്കുക", + "hide_extras": "എക്സ്ട്രാകൾ മറയ്ക്കുക (ഉദാ. പരാമർശങ്ങൾ)", + "remove_audio_note_transcript_capability": "ഓഡിയോ നോട്ട് ട്രാൻസ്ക്രിപ്റ്റ് ശേഷി നീക്കം ചെയ്യുക", + "hide_caption_text": "അടിക്കുറിപ്പ് വാചകം മറയ്ക്കുക", + "remove_audio_note_duration": "ഓഡിയോ നോട്ട് ദൈർഘ്യം നീക്കം ചെയ്യുക" + }, + "hide_ui_components": { + "hide_chat_call_buttons": "ചാറ്റ് കോൾ ബട്ടണുകൾ നീക്കം ചെയ്യുക", + "hide_live_location_share_button": "തത്സമയ ലൊക്കേഷൻ പങ്കിടൽ ബട്ടൺ നീക്കം ചെയ്യുക", + "hide_voice_record_button": "വോയ്സ് റെക്കോർഡ് ബട്ടൺ നീക്കം ചെയ്യുക", + "hide_profile_call_buttons": "പ്രൊഫൈൽ കോൾ ബട്ടണുകൾ നീക്കം ചെയ്യുക", + "hide_stickers_button": "സ്റ്റിക്കറുകൾ ബട്ടൺ നീക്കം ചെയ്യുക", + "hide_unread_chat_hint": "വായിക്കാത്ത ചാറ്റ് സൂചന നീക്കം ചെയ്യുക" + }, + "home_tab": { + "map": "മാപ്പ്", + "chat": "ചാറ്റ്", + "camera": "ക്യാമറ", + "discover": "കണ്ടെത്തുക", + "spotlight": "സ്പോട്ട്ലൈറ്റ്" + }, + "add_friend_source_spoof": { + "added_by_mention": "പരാമർശം വഴി", + "added_by_group_chat": "ഗ്രൂപ്പ് ചാറ്റ് വഴി", + "added_by_qr_code": "QR കോഡ് വഴി", + "added_by_community": "കമ്മ്യൂണിറ്റി പ്രകാരം", + "added_by_quick_add": "ദ്രുത കൂട്ടിച്ചേർക്കൽ വഴി", + "added_by_username": "ഉപയോക്തൃനാമം പ്രകാരം" + }, + "bypass_video_length_restriction": { + "single": "ഏക മാധ്യമം", + "split": "സ്പ്ലിറ്റ് മീഡിയ" + }, + "old_bitmoji_selfie": { + "2d": "2D ബിറ്റ്‌മോജി", + "3d": "3D ബിറ്റ്‌മോജി" + }, + "disable_confirmation_dialogs": { + "block_friend": "സുഹൃത്തിനെ തടയുക", + "ignore_friend": "സുഹൃത്തിനെ അവഗണിക്കുക", + "hide_friend": "സുഹൃത്തിനെ മറയ്ക്കുക", + "hide_conversation": "സംഭാഷണം മറയ്ക്കുക", + "remove_friend": "സുഹൃത്തിനെ നീക്കം ചെയ്യുക", + "clear_conversation": "ഫ്രണ്ട് ഫീഡിൽ നിന്ന് സംഭാഷണം മായ്‌ക്കുക" + }, + "auto_reload": { + "snapchat_only": "Snapchat മാത്രം", + "all": "എല്ലാം (Snapchat SnapEnhance)" + }, + "edit_text_override": { + "multi_line_chat_input": "മൾട്ടി ലൈൻ ചാറ്റ് ഇൻപുട്ട്", + "bypass_text_input_limit": "ബൈപാസ് ടെക്സ്റ്റ് ഇൻപുട്ട് പരിധി" + }, + "auto_purge": { + "never": "ഒരിക്കലുമില്ല", + "1_hour": "1 മണിക്കൂർ", + "3_hours": "3 മണിക്കൂർ", + "6_hours": "6 മണിക്കൂർ", + "12_hours": "12 മണിക്കൂർ", + "1_day": "1 ദിവസം", + "3_days": "3 ദിവസം", + "1_week": "1 ആഴ്ച", + "2_weeks": "2 ആഴ്ച", + "1_month": "1 മാസം", + "3_months": "3 മാസം", + "6_months": "6 മാസം" + }, + "app_appearance": { + "always_light": "എപ്പോഴും വെളിച്ചം", + "always_dark": "എപ്പോഴും ഇരുട്ട്" + } + } + }, + "actions": { + "export_chat_messages": "ചാറ്റ് സന്ദേശങ്ങൾ കയറ്റുമതി ചെയ്യുക", + "check_for_updates": "അപ്ഡേറ്റുകൾക്കായി പരിശോധിക്കുക", + "clean_snapchat_cache": "Snapchat കാഷെ വൃത്തിയാക്കുക", + "export_memories": "ഓർമ്മകൾ കയറ്റുമതി ചെയ്യുക", + "bulk_messaging_action": "ബൾക്ക് മെസേജിംഗ് ആക്ഷൻ", + "refresh_mappings": "മാപ്പിംഗുകൾ പുതുക്കുക", + "clear_message_logger": "സന്ദേശ ലോഗർ മായ്‌ക്കുക", + "open_map": "മാപ്പിൽ ലൊക്കേഷൻ തിരഞ്ഞെടുക്കുക" + }, + "content_type": { + "NOTE": "ഓഡിയോ കുറിപ്പ്", + "STATUS": "പദവി", + "STATUS_SAVE_TO_CAMERA_ROLL": "ക്യാമറ റോളിൽ സംരക്ഷിച്ചു", + "STATUS_CONVERSATION_CAPTURE_SCREENSHOT": "സ്ക്രീൻഷോട്ട്", + "STATUS_CONVERSATION_CAPTURE_RECORD": "സ്ക്രീൻ റെക്കോർഡ്", + "STATUS_CALL_MISSED_VIDEO": "മിസ്‌ഡ് വീഡിയോ കോൾ", + "SNAP": "സ്നാപ്പ്", + "STATUS_COUNTDOWN": "കൗണ്ട്ഡൗൺ", + "STICKER": "സ്റ്റിക്കർ", + "LOCATION": "സ്ഥാനം", + "STATUS_CALL_MISSED_AUDIO": "മിസ്‌ഡ് ഓഡിയോ കോൾ", + "CHAT": "ചാറ്റ്", + "EXTERNAL_MEDIA": "ബാഹ്യ മാധ്യമങ്ങൾ", + "CREATIVE_TOOL_ITEM": "ക്രിയേറ്റീവ് ടൂൾ ഇനം", + "FAMILY_CENTER_INVITE": "കുടുംബ കേന്ദ്രം ക്ഷണം", + "FAMILY_CENTER_ACCEPT": "കുടുംബ കേന്ദ്രം സ്വീകരിക്കുക", + "FAMILY_CENTER_LEAVE": "ഫാമിലി സെന്റർ ലീവ്", + "STATUS_PLUS_GIFT": "സ്റ്റാറ്റസ് പ്ലസ് സമ്മാനം", + "TINY_SNAP": "ചെറിയ സ്നാപ്പ്", + "LIVE_LOCATION_SHARE": "തത്സമയ ലൊക്കേഷൻ പങ്കിടുക" + }, + "profile_info": { + "snapchat_plus_state": { + "not_subscribed": "സബ്സ്ക്രൈബ് ചെയ്തിട്ടില്ല", + "subscribed": "സബ്സ്ക്രൈബ് ചെയ്തു" + }, + "title": "പ്രൊഫൈൽ വിവരം", + "snapchat_plus": "സ്നാപ്ചാറ്റ് പ്ലസ്", + "friendship": "സൗഹൃദം", + "add_source": "ഉറവിടം ചേർക്കുക", + "birthday": "ജന്മദിനം : {മാസം} {ദിവസം}", + "display_name": "പ്രദർശന നാമം", + "added_date": "ചേർത്ത തീയതി", + "first_created_username": "ആദ്യം സൃഷ്ടിച്ച ഉപയോക്തൃനാമം", + "hidden_birthday": "ജന്മദിനം: മറഞ്ഞിരിക്കുന്നു", + "mutable_username": "മാറ്റാവുന്ന ഉപയോക്തൃനാമം" + }, + "friendship_link_type": { + "mutual": "പരസ്പരമുള്ള", + "blocked": "തടഞ്ഞു", + "deleted": "ഇല്ലാതാക്കി", + "following": "പിന്തുടരുന്നു", + "suggested": "നിർദ്ദേശിച്ചു", + "incoming_follower": "ഇൻകമിംഗ് ഫോളോവർ", + "outgoing": "ഔട്ട്ഗോയിംഗ്", + "incoming": "ഇൻകമിംഗ്" + }, + "media_download_source": { + "public_story": "പൊതു കഥ", + "spotlight": "സ്പോട്ട്ലൈറ്റ്", + "profile_picture": "പ്രൊഫൈൽ ചിത്രം", + "chat_media": "ചാറ്റ് മീഡിയ", + "story": "കഥ", + "merged": "ലയിപ്പിച്ചു", + "none": "ഒന്നുമില്ല", + "pending": "തീർപ്പാക്കാത്തത്", + "story_logger": "സ്റ്റോറി ലോഗർ" + }, + "chat_action_menu": { + "download_button": "ഡൗൺലോഡ്", + "delete_logged_message_button": "ലോഗിൻ ചെയ്ത സന്ദേശം ഇല്ലാതാക്കുക", + "preview_button": "പ്രിവ്യൂ", + "convert_message": "സന്ദേശം പരിവർത്തനം ചെയ്യുക" + }, + "modal_option": { + "close": "അടയ്ക്കുക", + "profile_info": "പ്രൊഫൈൽ വിവരം" + }, + "conversation_preview": { + "streak_expiration": "{day} ദിവസം {hour} മണിക്കൂർ {minute} മിനിറ്റിനുള്ളിൽ കാലഹരണപ്പെടുന്നു", + "total_messages": "ആകെ അയച്ച/ലഭിച്ച സന്ദേശങ്ങൾ: {count}", + "title": "പ്രിവ്യൂ", + "unknown_user": "അജ്ഞാത ഉപയോക്താവ്" + }, + "opera_context_menu": { + "created_at": "{date}-ന് സൃഷ്‌ടിച്ചത്", + "expires_at": "{date}-ന് കാലഹരണപ്പെടുന്നു", + "media_duration": "മീഡിയ ദൈർഘ്യം: {duration} ms", + "show_debug_info": "ഡീബഗ് വിവരം കാണിക്കുക", + "sent_at": "{date}-ന് അയച്ചു", + "media_size": "മീഡിയ വലുപ്പം: {size}", + "download": "മീഡിയ ഡൗൺലോഡ് ചെയ്യുക" + }, + "bulk_messaging_action": { + "progress_status": "{total}-ന്റെ {index} പ്രോസസ്സ് ചെയ്യുന്നു", + "selection_dialog_continue_button": "തുടരുക", + "actions": { + "remove_friends": "സുഹൃത്തുക്കളെ നീക്കം ചെയ്യുക", + "clear_conversations": "വ്യക്തമായ സംഭാഷണങ്ങൾ" + }, + "confirmation_dialog": { + "message": "ഇത് തിരഞ്ഞെടുത്ത എല്ലാ സുഹൃത്തുക്കളെയും ബാധിക്കും. ഈ പ്രവർത്തനം പഴയപടിയാക്കാനാകില്ല.", + "title": "നിങ്ങൾക്ക് ഉറപ്പാണോ?" + }, + "choose_action_title": "ഒരു പ്രവർത്തനം തിരഞ്ഞെടുക്കുക" + }, + "chat_export": { + "exporter_dialog": { + "select_conversations_title": "സംഭാഷണങ്ങൾ തിരഞ്ഞെടുക്കുക", + "text_field_selection": "{തുക} തിരഞ്ഞെടുത്തു", + "text_field_selection_all": "എല്ലാം", + "export_file_format_title": "ഫയൽ ഫോർമാറ്റ് കയറ്റുമതി ചെയ്യുക", + "download_medias_title": "Medias ഡൗൺലോഡ് ചെയ്യുക", + "message_type_filter_title": "തരം അനുസരിച്ച് സന്ദേശങ്ങൾ ഫിൽട്ടർ ചെയ്യുക", + "amount_of_messages_title": "സന്ദേശങ്ങളുടെ അളവ് (എല്ലാവർക്കും ശൂന്യമായി വിടുക)" + }, + "dialog_negative_button": "റദ്ദാക്കുക", + "dialog_positive_button": "കയറ്റുമതി", + "exported_to": "{path}-ലേക്ക് കയറ്റുമതി ചെയ്തു", + "exporting_chats": "ചാറ്റുകൾ കയറ്റുമതി ചെയ്യുന്നു...", + "export_fail": "സംഭാഷണം {conversation} കയറ്റുമതി ചെയ്യാനായില്ല", + "writing_output": "ഔട്ട്പുട്ട് എഴുതുന്നു...", + "finished": "ചെയ്തു! നിങ്ങൾക്ക് ഇപ്പോൾ ഈ ഡയലോഗ് അടയ്ക്കാം.", + "no_messages_found": "സന്ദേശങ്ങളൊന്നും കണ്ടെത്തിയില്ല!", + "exporting_message": "{conversation} എക്‌സ്‌പോർട്ട് ചെയ്യുന്നു...", + "processing_chats": "{amount} സംഭാഷണങ്ങൾ പ്രോസസ്സ് ചെയ്യുന്നു..." + }, + "button": { + "ok": "ശരി", + "positive": "അതെ", + "negative": "ഇല്ല", + "cancel": "റദ്ദാക്കുക", + "download": "ഡൗൺലോഡ്", + "open": "തുറക്കുക" + }, + "better_notifications": { + "button": { + "reply": "മറുപടി", + "download": "ഡൗൺലോഡ്", + "mark_as_read": "വായിച്ചതായി അടയാളപ്പെടുത്തുക" + }, + "stealth_mode_notice": "സ്റ്റെൽത്ത് മോഡിൽ വായിച്ചതായി അടയാളപ്പെടുത്താൻ കഴിയില്ല" + }, + "profile_picture_downloader": { + "button": "പ്രൊഫൈൽ ചിത്രം ഡൗൺലോഡ് ചെയ്യുക", + "avatar_option": "അവതാർ", + "background_option": "പശ്ചാത്തലം", + "title": "പ്രൊഫൈൽ ചിത്രം ഡൗൺലോഡർ" + }, + "call_start_confirmation": { + "dialog_title": "കോൾ ആരംഭിക്കുക", + "dialog_message": "നിങ്ങൾക്ക് ഒരു കോൾ ആരംഭിക്കണമെന്ന് തീർച്ചയാണോ?" + }, + "half_swipe_notifier": { + "notification_channel_name": "പകുതി സ്വൈപ്പ് ചെയ്യുക", + "notification_content_group": "{friend} {group}-ലേക്ക് {duration} സെക്കൻഡ് പകുതി സ്വൈപ്പ് ചെയ്തു", + "notification_content_dm": "{സുഹൃത്ത്} നിങ്ങളുടെ ചാറ്റിലേക്ക് {duration} സെക്കൻഡ് പകുതി സ്വൈപ്പ് ചെയ്തു" + }, + "download_processor": { + "attachment_type": { + "snap": "സ്നാപ്പ്", + "sticker": "സ്റ്റിക്കർ", + "external_media": "ബാഹ്യ മാധ്യമങ്ങൾ", + "note": "കുറിപ്പ്", + "original_story": "ഒറിജിനൽ സ്റ്റോറി" + }, + "download_started_toast": "ഡൗൺലോഡ് ആരംഭിച്ചു", + "unsupported_content_type_toast": "പിന്തുണയ്‌ക്കാത്ത ഉള്ളടക്ക തരം!", + "failed_no_longer_available_toast": "മീഡിയ ഇനി ലഭ്യമല്ല", + "no_attachments_toast": "അറ്റാച്ചുമെന്റുകളൊന്നും കണ്ടെത്തിയില്ല!", + "already_queued_toast": "മാധ്യമങ്ങൾ ഇതിനകം ക്യൂവിലാണ്!", + "already_downloaded_toast": "മീഡിയ ഇതിനകം ഡൗൺലോഡ് ചെയ്‌തു!", + "saved_toast": "{path}-ലേക്ക് സംരക്ഷിച്ചു", + "download_toast": "{path} ഡൗൺലോഡ് ചെയ്യുന്നു...", + "failed_generic_toast": "ഡൗൺലോഡ് ചെയ്യാനായില്ല", + "failed_to_create_preview_toast": "പ്രിവ്യൂ സൃഷ്ടിക്കുന്നതിൽ പരാജയപ്പെട്ടു", + "failed_gallery_toast": "ഗാലറിയിൽ സംരക്ഷിക്കുന്നതിൽ പരാജയപ്പെട്ടു {പിശക്}", + "select_attachments_title": "ഡൗൺലോഡ് ചെയ്യാൻ അറ്റാച്ച്‌മെന്റുകൾ തിരഞ്ഞെടുക്കുക", + "processing_toast": "{path} പ്രോസസ്സ് ചെയ്യുന്നു...", + "failed_processing_toast": "പ്രോസസ്സിംഗ് പരാജയപ്പെട്ടു {പിശക്}" + }, + "streaks_reminder": { + "notification_title": "വരകൾ", + "notification_text": "{hoursLeft} മണിക്കൂറിനുള്ളിൽ {സുഹൃത്തുമായുള്ള നിങ്ങളുടെ സ്ട്രീക്ക് നിങ്ങൾക്ക് നഷ്ടപ്പെടും" + }, + "suspend_location_updates": { + "switch_text": "ലൊക്കേഷൻ അപ്‌ഡേറ്റുകൾ താൽക്കാലികമായി നിർത്തുക" + }, + "gallery_media_send_override": { + "multiple_media_toast": "നിങ്ങൾക്ക് ഒരു സമയം ഒരു മീഡിയ മാത്രമേ അയയ്ക്കാൻ കഴിയൂ" + }, + "friend_menu_option": { + "mark_snaps_as_seen": "Snaps കണ്ടതായി അടയാളപ്പെടുത്തുക", + "mark_stories_as_seen_locally": "പ്രാദേശികമായി കാണുന്ന കഥകൾ അടയാളപ്പെടുത്തുക", + "preview": "പ്രിവ്യൂ", + "stealth_mode": "സ്റ്റെൽത്ത് മോഡ്", + "auto_download_blacklist": "ഓട്ടോ ഡൗൺലോഡ് ബ്ലാക്ക്‌ലിസ്റ്റ്", + "anti_auto_save": "ആന്റി ഓട്ടോ സേവ്" + } +} diff --git a/common/src/main/assets/lang/nl.json b/common/src/main/assets/lang/nl.json index 56d7eee41..1ff326b24 100644 --- a/common/src/main/assets/lang/nl.json +++ b/common/src/main/assets/lang/nl.json @@ -228,10 +228,6 @@ "name": "Snapreeksherstel verbergen", "description": "Verbergt de Herstel knop in de vrienden feed" }, - "hide_story_sections": { - "name": "Verhalen Sectie verbergen", - "description": "Verberg bepaalde UI-elementen die worden weergegeven in het verhaalgedeelte" - }, "hide_ui_components": { "name": "Verberg UI Componenten", "description": "Selecteer welke UI componenten te verbergen" diff --git a/common/src/main/assets/lang/tr_TR.json b/common/src/main/assets/lang/tr_TR.json index 46cfb4020..d939a1a73 100644 --- a/common/src/main/assets/lang/tr_TR.json +++ b/common/src/main/assets/lang/tr_TR.json @@ -280,10 +280,6 @@ "name": "Seri Geri Yüklemeyi Gizle", "description": "Arkadaş akışındaki Seri Geri Yükle düğmesini gizler" }, - "hide_story_sections": { - "name": "Hikaye Bölümünü Gizle", - "description": "Hikaye bölümünde gösterilen belirli arayüz öğelerini gizle" - }, "hide_ui_components": { "name": "Kullanıcı Arayüzü Bileşenlerini Gizle", "description": "Hangi kullanıcı arayüzü bileşenlerinin gizleneceğini seçin" @@ -510,10 +506,6 @@ "name": "Spotlight Yorumlar Kullanıcı Adı", "description": "Spotlight yorumlarında yazar kullanıcı adını gösterir" }, - "disable_public_stories": { - "name": "Halka Açık Hikayeleri Devre Dışı Bırak", - "description": "Herkese açık tüm hikayeleri Keşfet sayfasından kaldırır\nDüzgün çalışması için önbelleğin temizlenmesi gerekebilir" - }, "force_upload_source_quality": { "name": "Kaynak Kalitesini Yüklemeye Zorla", "description": "Snapchat'i medyayı orijinal kalitesinde yüklemeye zorlar\nLütfen bunun medyadan meta verileri kaldırmayabileceğini unutmayın" @@ -798,13 +790,6 @@ "hide_voice_record_button": "Ses Kayıt Butonunu Kaldır", "hide_unread_chat_hint": "Okunmamış Sohbet İpucunu Kaldır" }, - "hide_story_sections": { - "hide_friend_suggestions": "Arkadaş önerilerini gizle", - "hide_friends": "Arkadaşlar bölümünü gizle", - "hide_suggested": "Önerilen bölümünü gizle", - "hide_for_you": "Sizin İçin Bölümünü Gizle", - "hide_suggested_friend_stories": "Önerilen arkadaş hikayelerini gizle" - }, "home_tab": { "map": "Harita", "chat": "Sohbet", diff --git a/common/src/main/assets/lang/ur_IN.json b/common/src/main/assets/lang/ur_IN.json index 8667858db..27c30d3a9 100644 --- a/common/src/main/assets/lang/ur_IN.json +++ b/common/src/main/assets/lang/ur_IN.json @@ -27,7 +27,8 @@ "home_settings": "ترتیبات", "home_logs": "لاگز", "social": "سوشل", - "scripts": "اسکرپٹس" + "scripts": "اسکرپٹس", + "tasks": "کام" }, "sections": { "home": { @@ -49,6 +50,9 @@ "streaks_expiration_short": "{hours}گنٹے", "streaks_expiration_text": "میں ختم ہو جائے گا {eta}", "reminder_button": "یاد دلانے کی ترتیب دیں" + }, + "tasks": { + "no_tasks": "کوئی کام نہیں" } }, "dialogs": { @@ -58,6 +62,10 @@ "fetch_error": "ڈیٹا حاصل کرنے میں ناکامی", "category_groups": "گروپس", "category_friends": "دوست" + }, + "scripting_warning": { + "title": "وارننگ", + "content": "SnapEnhance میں ایک اسکرپٹنگ ٹول شامل ہے، جو آپ کے آلے پر صارف کے متعین کوڈ کے نفاذ کی اجازت دیتا ہے۔ انتہائی احتیاط برتیں اور صرف معروف، قابل اعتماد ذرائع سے ماڈیول انسٹال کریں۔ غیر مجاز یا غیر تصدیق شدہ ماڈیولز آپ کے سسٹم کے لیے حفاظتی خطرات پیدا کر سکتے ہیں۔" } } }, @@ -99,6 +107,9 @@ }, "pin_conversation": { "name": "بات چیت کو پن کریں" + }, + "unsaveable_messages": { + "name": "ناقابل محفوظ پیغامات" } } }, @@ -254,10 +265,6 @@ "name": "استیک کو بحال کرنے کا بٹن چھپائیں", "description": "دوست فیڈ میں بحال کرنے کا بٹن چھپائیں" }, - "hide_story_sections": { - "name": "کہانی کے حصے چھپائیں", - "description": "کہانی کے حصوں میں دکھائی جانے والی کچھ یو آئی عناصر چھپائیں" - }, "hide_ui_components": { "name": "یو آئی کے حصے چھپائیں", "description": "چھپانے کے لئے یو آئی کمپوننٹس منتخب کریں" @@ -578,12 +585,6 @@ "hide_stickers_button": "اسٹکر بٹن ہٹائیں", "hide_voice_record_button": "آواز ریکارڈ بٹن ہٹائیں" }, - "hide_story_sections": { - "hide_friend_suggestions": "دوست کی تجاویز چھپائیں", - "hide_friends": "دوستوں کو چھپائیں", - "hide_suggested": "مشورے شدہ حصے کو چھپائیں", - "hide_for_you": "آپ کے لئے حصے کو چھپائیں" - }, "home_tab": { "map": "نقشہ", "chat": "چیٹ", From 1f4eb18aa832630ee88951cad1d50d259c4228f2 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sat, 13 Jan 2024 19:23:39 +0100 Subject: [PATCH 50/53] feat: override camera resolution --- common/src/main/assets/lang/en_US.json | 22 ++++----- .../snapenhance/common/config/impl/Camera.kt | 49 ++++++++++++------- .../core/features/impl/tweaks/CameraTweaks.kt | 45 +++++++---------- .../rhunk/snapenhance/mapper/ClassMapper.kt | 7 ++- .../mapper/impl/ScCameraSettingsMapper.kt | 25 ---------- 5 files changed, 59 insertions(+), 89 deletions(-) delete mode 100644 mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScCameraSettingsMapper.kt diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 82902718a..fda3cce50 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -544,21 +544,17 @@ "name": "Immersive Preview", "description": "Prevents Snapchat from Cropping the Camera preview\nThis might cause the camera to flicker on some devices" }, - "override_preview_resolution": { - "name": "Override Preview Resolution", - "description": "Overrides the Camera Preview Resolution" + "override_front_resolution": { + "name": "Override Front Resolution", + "description": "Overrides the camera resolution for the front camera" }, - "override_picture_resolution": { - "name": "Override Picture Resolution", - "description": "Overrides the picture resolution" + "override_back_resolution": { + "name": "Override Back Resolution", + "description": "Overrides the camera resolution for the back camera" }, - "custom_preview_resolution": { - "name": "Custom Preview Resolution", - "description": "Sets a custom camera preview resolution, width x height (e.g. 1920x1080).\nThe custom resolution must be supported by your device" - }, - "custom_picture_resolution": { - "name": "Custom Picture Resolution", - "description": "Sets a custom picture resolution, width x height (e.g. 1920x1080).\nThe custom resolution must be supported by your device" + "custom_resolution": { + "name": "Custom Resolution", + "description": "Sets a custom camera resolution, width x height (e.g. 1920x1080).\nThe custom resolution must be supported by your device" }, "custom_frame_rate": { "name": "Custom Frame Rate", diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Camera.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Camera.kt index f267c607a..617d55965 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Camera.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Camera.kt @@ -15,26 +15,37 @@ class Camera : ConfigContainer() { private val defaultResolutions = listOf("3264x2448", "3264x1840", "3264x1504", "2688x1512", "2560x1920", "2448x2448", "2340x1080", "2160x1080", "1920x1440", "1920x1080", "1600x1200", "1600x960", "1600x900", "1600x736", "1600x720", "1560x720", "1520x720", "1440x1080", "1440x720", "1280x720", "1080x1080", "1080x720", "960x720", "720x720", "720x480", "640x480", "352x288", "320x240", "176x144").toTypedArray() } - private lateinit var _overridePreviewResolution: PropertyValue - private lateinit var _overridePictureResolution: PropertyValue + private lateinit var _overrideFrontResolution: PropertyValue + private lateinit var _overrideBackResolution: PropertyValue override fun lateInit(context: Context) { - val resolutions = runCatching { - if (context.packageName == Constants.SNAPCHAT_PACKAGE_NAME) return@runCatching null // prevent snapchat from crashing - context.getSystemService(CameraManager::class.java).run { - cameraIdList.flatMap { cameraId -> - getCameraCharacteristics(cameraId).get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)?.let { - it.outputFormats.flatMap { format -> it.getOutputSizes(format).toList() } - } ?: emptyList() - }.sortedByDescending { it.width * it.height }.map { "${it.width}x${it.height}" }.distinct().toTypedArray() + val backResolutions = mutableListOf() + val frontResolutions = mutableListOf() + + context.getSystemService(CameraManager::class.java).apply { + if (context.packageName == Constants.SNAPCHAT_PACKAGE_NAME) return@apply // prevent snapchat from crashing + + runCatching { + cameraIdList.forEach { cameraId -> + val characteristics = getCameraCharacteristics(cameraId) + val isSelfie = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT + + (frontResolutions.takeIf { isSelfie } ?: backResolutions).addAll( + characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)?.let { + it.outputFormats.flatMap { format -> it.getOutputSizes(format).toList() } + }?.sortedByDescending { it.width * it.height }?.map { "${it.width}x${it.height}" }?.distinct() ?: emptyList() + ) + } + }.onFailure { + AbstractLogger.directError("Failed to get camera resolutions", it) + backResolutions.addAll(defaultResolutions) + frontResolutions.addAll(defaultResolutions) } - }.onFailure { - AbstractLogger.directError("Failed to get camera resolutions", it) - }.getOrNull() ?: defaultResolutions + } - _overridePreviewResolution = unique("override_preview_resolution", *resolutions) + _overrideFrontResolution = unique("override_front_resolution", *frontResolutions.toTypedArray()) { addFlags(ConfigFlag.NO_TRANSLATE) } - _overridePictureResolution = unique("override_picture_resolution", *resolutions) + _overrideBackResolution = unique("override_back_resolution", *backResolutions.toTypedArray()) { addFlags(ConfigFlag.NO_TRANSLATE) } } @@ -46,8 +57,8 @@ class Camera : ConfigContainer() { ) { addNotices(FeatureNotice.UNSTABLE); addFlags(ConfigFlag.NO_TRANSLATE) } val hevcRecording = boolean("hevc_recording") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) } val forceCameraSourceEncoding = boolean("force_camera_source_encoding") - val overridePreviewResolution get() = _overridePreviewResolution - val overridePictureResolution get() = _overridePictureResolution - val customPreviewResolution = string("custom_preview_resolution") { addNotices(FeatureNotice.UNSTABLE); inputCheck = { it.matches(Regex("\\d+x\\d+")) } } - val customPictureResolution = string("custom_picture_resolution") { addNotices(FeatureNotice.UNSTABLE); inputCheck = { it.matches(Regex("\\d+x\\d+")) } } + val overrideFrontResolution get() = _overrideFrontResolution + val overrideBackResolution get() = _overrideBackResolution + + val customResolution = string("custom_resolution") { addNotices(FeatureNotice.UNSTABLE); inputCheck = { it.matches(Regex("\\d+x\\d+")) } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt index ed9ca14c9..70e83901b 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt @@ -9,15 +9,13 @@ import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics.Key import android.hardware.camera2.CameraManager import android.media.Image +import android.media.ImageReader import android.util.Range import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook -import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.ktx.setObjectField -import me.rhunk.snapenhance.core.wrapper.impl.ScSize -import me.rhunk.snapenhance.mapper.impl.ScCameraSettingsMapper import java.io.ByteArrayOutputStream import java.nio.ByteBuffer @@ -38,15 +36,25 @@ class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACT } } - CameraManager::class.java.hook("openCamera", HookStage.BEFORE) { param -> + } + + var isLastCameraFront = false + + CameraManager::class.java.hook("openCamera", HookStage.BEFORE) { param -> + if (config.disable.get()) { param.setResult(null) + return@hook } + val cameraManager = param.thisObject() as? CameraManager ?: return@hook + isLastCameraFront = cameraManager.getCameraCharacteristics(param.arg(0)).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT } - val previewResolutionConfig = config.customPreviewResolution.getNullable()?.takeIf { it.isNotEmpty() }?.let { parseResolution(it) } - ?: config.overridePreviewResolution.getNullable()?.let { parseResolution(it) } - val captureResolutionConfig = config.customPictureResolution.getNullable()?.takeIf { it.isNotEmpty() }?.let { parseResolution(it) } - ?: config.overridePictureResolution.getNullable()?.let { parseResolution(it) } + ImageReader::class.java.hook("newInstance", HookStage.BEFORE) { param -> + val captureResolutionConfig = config.customResolution.getNullable()?.takeIf { it.isNotEmpty() }?.let { parseResolution(it) } + ?: (if (isLastCameraFront) config.overrideFrontResolution.getNullable() else config.overrideBackResolution.getNullable())?.let { parseResolution(it) } ?: return@hook + param.setArg(0, captureResolutionConfig[0]) + param.setArg(1, captureResolutionConfig[1]) + } config.customFrameRate.getNullable()?.also { value -> val customFrameRate = value.toInt() @@ -63,25 +71,6 @@ class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACT } } - context.mappings.useMapper(ScCameraSettingsMapper::class) { - classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> - val previewResolution = ScSize(param.argNullable(2)) - val captureResolution = ScSize(param.argNullable(3)) - - if (previewResolution.isPresent() && captureResolution.isPresent()) { - previewResolutionConfig?.let { - previewResolution.first = it[0] - previewResolution.second = it[1] - } - - captureResolutionConfig?.let { - captureResolution.first = it[0] - captureResolution.second = it[1] - } - } - } - } - if (config.blackPhotos.get()) { findClass("android.media.ImageReader\$SurfaceImage").hook("getPlanes", HookStage.AFTER) { param -> val image = param.thisObject() as? Image ?: return@hook @@ -91,7 +80,7 @@ class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACT compress(Bitmap.CompressFormat.JPEG, 100, output) recycle() } - planes.filterNotNull().forEach { plane -> + planes.filterNotNull().forEach { plane -> plane.setObjectField("mBuffer", ByteBuffer.wrap(output.toByteArray())) } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt index 283d64d35..86843ba96 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt @@ -1,13 +1,13 @@ package me.rhunk.snapenhance.mapper +import com.android.tools.smali.dexlib2.Opcodes +import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile +import com.android.tools.smali.dexlib2.iface.ClassDef import com.google.gson.JsonObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.rhunk.snapenhance.mapper.impl.* -import com.android.tools.smali.dexlib2.Opcodes -import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile -import com.android.tools.smali.dexlib2.iface.ClassDef import java.io.BufferedInputStream import java.io.InputStream import java.util.zip.ZipFile @@ -26,7 +26,6 @@ class ClassMapper( MediaQualityLevelProviderMapper(), OperaPageViewControllerMapper(), PlusSubscriptionMapper(), - ScCameraSettingsMapper(), StoryBoostStateMapper(), FriendsFeedEventDispatcherMapper(), CompositeConfigurationProviderMapper(), diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScCameraSettingsMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScCameraSettingsMapper.kt deleted file mode 100644 index 2c0a77411..000000000 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScCameraSettingsMapper.kt +++ /dev/null @@ -1,25 +0,0 @@ -package me.rhunk.snapenhance.mapper.impl - -import me.rhunk.snapenhance.mapper.AbstractClassMapper -import me.rhunk.snapenhance.mapper.ext.findConstString -import me.rhunk.snapenhance.mapper.ext.getClassName -import me.rhunk.snapenhance.mapper.ext.getStaticConstructor -import me.rhunk.snapenhance.mapper.ext.isEnum - -class ScCameraSettingsMapper : AbstractClassMapper("ScCameraSettings") { - val classReference = classReference("class") - - init { - mapper { - for (clazz in classes) { - val firstConstructor = clazz.directMethods.firstOrNull { it.name == "" } ?: continue - if (firstConstructor.parameterTypes.size < 27) continue - val firstParameter = getClass(firstConstructor.parameterTypes[0]) ?: continue - if (!firstParameter.isEnum() || firstParameter.getStaticConstructor()?.implementation?.findConstString("CONTINUOUS_PICTURE") != true) continue - - classReference.set(clazz.getClassName()) - return@mapper - } - } - } -} \ No newline at end of file From 2905fe6e2dd21fba1d494e695e0921a15660e50c Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 14 Jan 2024 00:35:55 +0100 Subject: [PATCH 51/53] feat(camera): custom frame rate - experimental disable cameras --- common/src/main/assets/lang/en_US.json | 20 +++++-- .../snapenhance/common/config/impl/Camera.kt | 10 ++-- .../core/features/impl/tweaks/CameraTweaks.kt | 60 ++++++++++++++----- 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index fda3cce50..33f16febb 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -532,9 +532,9 @@ "name": "Camera", "description": "Adjust the right settings for the perfect snap", "properties": { - "disable_camera": { - "name": "Disable Camera", - "description": "Prevents Snapchat from using the cameras available on your device" + "disable_cameras": { + "name": "Disable Cameras", + "description": "Prevents Snapchat from using the selected cameras" }, "black_photos": { "name": "Black Photos", @@ -556,9 +556,13 @@ "name": "Custom Resolution", "description": "Sets a custom camera resolution, width x height (e.g. 1920x1080).\nThe custom resolution must be supported by your device" }, - "custom_frame_rate": { - "name": "Custom Frame Rate", - "description": "Overrides the camera frame rate" + "front_custom_frame_rate": { + "name": "Front Custom Frame Rate", + "description": "Overrides the front camera frame rate" + }, + "back_custom_frame_rate": { + "name": "Back Custom Frame Rate", + "description": "Overrides the back camera frame rate" }, "force_camera_source_encoding": { "name": "Force Camera Source Encoding", @@ -866,6 +870,10 @@ "friends": "Friends", "following": "Following", "discover": "Discover" + }, + "disable_cameras":{ + "front": "Front Camera", + "back": "Back Camera" } } }, diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Camera.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Camera.kt index 617d55965..3919adbfa 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Camera.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Camera.kt @@ -13,6 +13,7 @@ import me.rhunk.snapenhance.common.logger.AbstractLogger class Camera : ConfigContainer() { companion object { private val defaultResolutions = listOf("3264x2448", "3264x1840", "3264x1504", "2688x1512", "2560x1920", "2448x2448", "2340x1080", "2160x1080", "1920x1440", "1920x1080", "1600x1200", "1600x960", "1600x900", "1600x736", "1600x720", "1560x720", "1520x720", "1440x1080", "1440x720", "1280x720", "1080x1080", "1080x720", "960x720", "720x720", "720x480", "640x480", "352x288", "320x240", "176x144").toTypedArray() + private val customFrameRates = arrayOf("5", "10", "20", "25", "30", "48", "60", "90", "120") } private lateinit var _overrideFrontResolution: PropertyValue @@ -49,13 +50,12 @@ class Camera : ConfigContainer() { { addFlags(ConfigFlag.NO_TRANSLATE) } } - val disable = boolean("disable_camera") + val disableCameras = multiple("disable_cameras", "front", "back") { addNotices(FeatureNotice.INTERNAL_BEHAVIOR); requireRestart() } val immersiveCameraPreview = boolean("immersive_camera_preview") { addNotices(FeatureNotice.UNSTABLE) } val blackPhotos = boolean("black_photos") - val customFrameRate = unique("custom_frame_rate", - "5", "10", "20", "25", "30", "48", "60", "90", "120" - ) { addNotices(FeatureNotice.UNSTABLE); addFlags(ConfigFlag.NO_TRANSLATE) } - val hevcRecording = boolean("hevc_recording") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) } + val frontCustomFrameRate = unique("front_custom_frame_rate", *customFrameRates) { requireRestart(); addFlags(ConfigFlag.NO_TRANSLATE) } + val backCustomFrameRate = unique("back_custom_frame_rate", *customFrameRates) { requireRestart(); addFlags(ConfigFlag.NO_TRANSLATE) } + val hevcRecording = boolean("hevc_recording") { requireRestart() } val forceCameraSourceEncoding = boolean("force_camera_source_encoding") val overrideFrontResolution get() = _overrideFrontResolution val overrideBackResolution get() = _overrideBackResolution diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt index 70e83901b..bedeb13c9 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt @@ -28,25 +28,41 @@ class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACT @SuppressLint("MissingPermission", "DiscouragedApi") override fun onActivityCreate() { val config = context.config.camera - if (config.disable.get()) { + + val frontCameraId = runCatching { context.androidContext.getSystemService(CameraManager::class.java).run { + cameraIdList.firstOrNull { getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT } + } }.getOrNull() + + if (config.disableCameras.get().isNotEmpty() && frontCameraId != null) { ContextWrapper::class.java.hook("checkPermission", HookStage.BEFORE) { param -> val permission = param.arg(0) if (permission == Manifest.permission.CAMERA) { param.setResult(PackageManager.PERMISSION_GRANTED) } } - } var isLastCameraFront = false CameraManager::class.java.hook("openCamera", HookStage.BEFORE) { param -> - if (config.disable.get()) { + val cameraManager = param.thisObject() as? CameraManager ?: return@hook + val cameraId = param.arg(0) + val disabledCameras = config.disableCameras.get() + + if (disabledCameras.size >= 2) { param.setResult(null) return@hook } - val cameraManager = param.thisObject() as? CameraManager ?: return@hook - isLastCameraFront = cameraManager.getCameraCharacteristics(param.arg(0)).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT + + isLastCameraFront = cameraId == frontCameraId + + if (disabledCameras.size != 1) return@hook + + // trick to replace unwanted camera with another one + if ((disabledCameras.contains("front") && isLastCameraFront) || (disabledCameras.contains("back") && !isLastCameraFront)) { + param.setArg(0, cameraManager.cameraIdList.filterNot { it == cameraId }.firstOrNull() ?: return@hook) + isLastCameraFront = !isLastCameraFront + } } ImageReader::class.java.hook("newInstance", HookStage.BEFORE) { param -> @@ -56,19 +72,33 @@ class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACT param.setArg(1, captureResolutionConfig[1]) } - config.customFrameRate.getNullable()?.also { value -> - val customFrameRate = value.toInt() - CameraCharacteristics::class.java.hook("get", HookStage.AFTER) { param -> - val key = param.arg>(0) - if (key == CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) { - val fpsRanges = param.getResult() as? Array<*> ?: return@hook - fpsRanges.forEach { - val range = it as? Range<*> ?: return@forEach - range.setObjectField("mUpper", customFrameRate) - range.setObjectField("mLower", customFrameRate) + CameraCharacteristics::class.java.hook("get", HookStage.AFTER) { param -> + val key = param.argNullable>(0) ?: return@hook + + if (key == CameraCharacteristics.LENS_FACING) { + val disabledCameras = config.disableCameras.get() + //FIXME: unexpected behavior when app is resumed + if (disabledCameras.size == 1) { + val isFrontCamera = param.getResult() as? Int == CameraCharacteristics.LENS_FACING_FRONT + if ((disabledCameras.contains("front") && isFrontCamera) || (disabledCameras.contains("back") && !isFrontCamera)) { + param.setResult(if (isFrontCamera) CameraCharacteristics.LENS_FACING_BACK else CameraCharacteristics.LENS_FACING_FRONT) } } } + + if (key == CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) { + val isFrontCamera = param.invokeOriginal( + arrayOf(CameraCharacteristics.LENS_FACING) + ) == CameraCharacteristics.LENS_FACING_FRONT + val customFrameRate = (if (isFrontCamera) config.frontCustomFrameRate.getNullable() else config.backCustomFrameRate.getNullable())?.toIntOrNull() ?: return@hook + val fpsRanges = param.getResult() as? Array<*> ?: return@hook + + fpsRanges.forEach { + val range = it as? Range<*> ?: return@forEach + range.setObjectField("mUpper", customFrameRate) + range.setObjectField("mLower", customFrameRate) + } + } } if (config.blackPhotos.get()) { From b17a8d245418d9352c0bfcdfb77390ecb890274b Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 14 Jan 2024 11:33:35 +0100 Subject: [PATCH 52/53] feat(core/e2ee): sent messages indicator - add warn message for decryption failure --- .../impl/experiments/EndToEndEncryption.kt | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt index 7dc1d20a7..2ad97950f 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import me.rhunk.snapenhance.common.data.ContentType -import me.rhunk.snapenhance.common.data.MessageState import me.rhunk.snapenhance.common.data.MessagingRuleType import me.rhunk.snapenhance.common.data.RuleState import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor @@ -33,9 +32,14 @@ import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.ui.addForegroundDrawable import me.rhunk.snapenhance.core.ui.removeForegroundDrawable import me.rhunk.snapenhance.core.util.EvictingMap +import me.rhunk.snapenhance.core.util.hook.HookStage +import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull import me.rhunk.snapenhance.core.wrapper.impl.MessageContent +import me.rhunk.snapenhance.core.wrapper.impl.MessageDestinations import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.mapper.impl.CallbackMapper import me.rhunk.snapenhance.nativelib.NativeLib import java.security.MessageDigest import kotlin.random.Random @@ -282,6 +286,16 @@ class EndToEndEncryption : MessagingRuleFeature( encryptedMessages.add(clientMessageId) } + fun setWarningMessage() { + encryptedMessages.add(clientMessageId) + outputContentType = ContentType.CHAT + outputBuffer = ProtoWriter().apply { + from(2) { + addString(1, "Failed to decrypt message, id=$clientMessageId. Check logs for more details.") + } + }.toByteArray() + } + fun replaceMessageText(text: String) { outputBuffer = ProtoWriter().apply { from(2) { @@ -309,6 +323,7 @@ class EndToEndEncryption : MessagingRuleFeature( val participantId = conversationParticipants.firstOrNull { participantIdHash.contentEquals(hashParticipantId(it, iv)) } ?: return@eachBuffer setDecryptedMessage(e2eeInterface.decryptMessage(participantId, ciphertext, iv) ?: run { context.log.warn("Failed to decrypt message for participant $participantId") + setWarningMessage() return@eachBuffer }) return@eachBuffer @@ -317,18 +332,13 @@ class EndToEndEncryption : MessagingRuleFeature( if (!participantIdHash.contentEquals(hashParticipantId(context.database.myUserId, iv))) return@eachBuffer setDecryptedMessage(e2eeInterface.decryptMessage(senderId, ciphertext, iv)?: run { - context.log.warn("Failed to decrypt message") + setWarningMessage() return@eachBuffer }) } }.onFailure { context.log.error("Failed to decrypt message id: $clientMessageId", it) - outputContentType = ContentType.CHAT - outputBuffer = ProtoWriter().apply { - from(2) { - addString(1, "Failed to decrypt message, id=$clientMessageId. Check logcat for more details.") - } - }.toByteArray() + setWarningMessage() } return@followPath @@ -492,9 +502,23 @@ class EndToEndEncryption : MessagingRuleFeature( override fun init() { if (!isEnabled) return + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("ConversationManagerDelegate")?.hook("onSendComplete", HookStage.BEFORE) { param -> + val sendMessageResult = param.arg(0) + val messageDestinations = MessageDestinations(sendMessageResult.getObjectField("mCompletedDestinations") ?: return@hook) + if (messageDestinations.mPhoneNumbers?.isNotEmpty() == true || messageDestinations.stories?.isNotEmpty() == true) return@hook + + val completedConversationDestinations = sendMessageResult.getObjectField("mCompletedConversationDestinations") as? ArrayList<*> ?: return@hook + val messageIds = completedConversationDestinations.filter { getState(SnapUUID(it.getObjectField("mConversationId")).toString()) }.mapNotNull { + it.getObjectFieldOrNull("mMessageId") as? Long + } + + encryptedMessages.addAll(messageIds) + } + } + context.event.subscribe(BuildMessageEvent::class, priority = 0) { event -> val message = event.message - if (message.messageState != MessageState.COMMITTED) return@subscribe val conversationId = message.messageDescriptor!!.conversationId.toString() messageHook( conversationId = conversationId, From e9184f5244e29e1ae80967710ed4a7ecdaebf6c4 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 14 Jan 2024 11:34:19 +0100 Subject: [PATCH 53/53] build: install to multiple devices --- app/build.gradle.kts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6392370c4..652ed667e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,9 @@ import com.android.build.gradle.internal.api.BaseVariantOutputImpl +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.gradle.configurationcache.extensions.capitalized +import java.io.ByteArrayOutputStream plugins { alias(libs.plugins.androidApplication) @@ -153,12 +157,27 @@ dependencies { afterEvaluate { properties["debug_flavor"]?.toString()?.let { tasks.findByName("install${it.capitalized()}Debug") }?.doLast { runCatching { - exec { - commandLine("adb", "shell", "am", "force-stop", properties["debug_package_name"]) + val devices = ByteArrayOutputStream().also { + exec { + commandLine("adb", "devices") + standardOutput = it + } + }.toString().lines().drop(1).mapNotNull { + line -> line.split("\t").firstOrNull()?.takeIf { it.isNotEmpty() } } - Thread.sleep(1000L) - exec { - commandLine("adb", "shell", "am", "start", properties["debug_package_name"]) + + runBlocking { + devices.forEach { device -> + launch { + exec { + commandLine("adb", "-s", device, "shell", "am", "force-stop", properties["debug_package_name"]) + } + delay(500) + exec { + commandLine("adb", "-s", device, "shell", "am", "start", properties["debug_package_name"]) + } + } + } } } }