diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1824cb64..c170d84c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -113,7 +113,6 @@ android { compose = true buildConfig = true } - signingConfigs { create("release") { storeFile = File(System.getProperty("user.home"), ".android/purrfectsnap-release.keystore") @@ -166,6 +165,9 @@ android { val releaseKeyAlias = gradleOrEnv("PS_RELEASE_KEY_ALIAS", providers) if (releaseStore.exists() && !releaseStorePass.isNullOrBlank() && !releaseKeyAlias.isNullOrBlank()) { signingConfig = signingConfigs.getByName("release") + } else { + // Keep local release builds installable when private release credentials are unavailable. + signingConfig = signingConfigs.getByName("debug") } } debug { @@ -356,6 +358,7 @@ afterEvaluate { } } } + } properties["debug_flavor"]?.let { diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/bridge/BridgeService.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/bridge/BridgeService.kt index 83b87b4d..88e661b6 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/bridge/BridgeService.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/bridge/BridgeService.kt @@ -5,6 +5,9 @@ import android.content.Intent import android.os.IBinder import android.os.ParcelFileDescriptor import android.os.RemoteException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import me.eternal.purrfectsnap.RemoteSideContext import me.eternal.purrfectsnap.SharedContextHolder @@ -219,19 +222,42 @@ class BridgeService : Service() { triggerScopeSync(SocialScope.getByName(scope), id, true) } + private val friendAccumulator = mutableListOf() + private val groupAccumulator = mutableListOf() + override fun passGroupsAndFriends( groups: List, - friends: List + friends: List, + chunkIndex: Int, + totalChunks: Int ) { - remoteSideContext.log.verbose("Received ${groups.size} groups and ${friends.size} friends") - val parsedFriends = friends.mapNotNull { toParcelable(it) } - val parsedGroups = groups.mapNotNull { toParcelable(it) } - pendingSocialSnapshotCallback?.let { callback -> - pendingSocialSnapshotCallback = null - callback(parsedFriends, parsedGroups) + synchronized(friendAccumulator) { + if (chunkIndex == 0) { + friendAccumulator.clear() + groupAccumulator.clear() + } + + remoteSideContext.log.verbose("Received chunk $chunkIndex/$totalChunks: ${groups.size} groups, ${friends.size} friends") + friendAccumulator.addAll(friends.mapNotNull { toParcelable(it) }) + groupAccumulator.addAll(groups.mapNotNull { toParcelable(it) }) + + if (chunkIndex == totalChunks - 1) { + val finalFriends = friendAccumulator.toList() + val finalGroups = groupAccumulator.toList() + + friendAccumulator.clear() + groupAccumulator.clear() + + remoteSideContext.coroutineScope.launch(Dispatchers.IO) { + pendingSocialSnapshotCallback?.let { callback -> + pendingSocialSnapshotCallback = null + callback(finalFriends, finalGroups) + } + remoteSideContext.database.replaceMessagingData(finalFriends, finalGroups) + remoteSideContext.database.messagingDataFlow.tryEmit(finalFriends to finalGroups) + } + } } - remoteSideContext.database.replaceMessagingData(parsedFriends, parsedGroups) - remoteSideContext.database.receiveMessagingDataCallback(parsedFriends, parsedGroups) } override fun getScopeNotes(id: String): String? { diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/download/DownloadProcessor.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/download/DownloadProcessor.kt index d3b94b15..ce74351a 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/download/DownloadProcessor.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/download/DownloadProcessor.kt @@ -283,7 +283,7 @@ class DownloadProcessor ( while (true) { val existingFile = outputFileFolder.findFile(finalFileName) ?: break - if (existingFile.length() == inputFile.length()) { + if (existingFile.length() == inputFile.length() && !remoteSideContext.config.root.downloader.allowDuplicate.get()) { val existingInputStream = remoteSideContext.androidContext.contentResolver.openInputStream(existingFile.uri) if (existingInputStream != null && streamsMatch(existingInputStream, inputFile.inputStream())) { return GallerySaveResult(existingFile.uri, alreadyDownloaded = true) @@ -376,7 +376,7 @@ class DownloadProcessor ( var destFile = File(destDir, fileName) var suffix = 1 while (destFile.exists()) { - if (destFile.length() == inputFile.length() && streamsMatch(destFile.inputStream(), inputFile.inputStream())) { + if (destFile.length() == inputFile.length() && !remoteSideContext.config.root.downloader.allowDuplicate.get() && streamsMatch(destFile.inputStream(), inputFile.inputStream())) { return GallerySaveResult(Uri.fromFile(destFile), alreadyDownloaded = true) } destFile = File(destDir, appendNameSuffix(fileName, suffix++)) diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt index 5e4b9dd0..78fbc040 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt @@ -151,31 +151,37 @@ class FFMpegProcessor( } val outputArguments = ArgumentList().apply { - this += "-preset" to (ffmpegOptions.preset.getNullable() ?: "ultrafast") - this += "-c:v" to (ffmpegOptions.customVideoCodec.get().takeIf { it.isNotEmpty() }?.lowercase() ?: "libx264") this += "-c:a" to (ffmpegOptions.customAudioCodec.get().takeIf { it.isNotEmpty() }?.lowercase() ?: "copy") - this += "-crf" to ffmpegOptions.constantRateFactor.get().let { "\"$it\"" } - this += "-b:v" to ffmpegOptions.videoBitrate.get().toString() + "K" this += "-b:a" to ffmpegOptions.audioBitrate.get().toString() + "K" } + fun applyVideoArguments() { + outputArguments += "-preset" to (ffmpegOptions.preset.getNullable() ?: "ultrafast") + outputArguments += "-c:v" to (ffmpegOptions.customVideoCodec.get().takeIf { it.isNotEmpty() }?.lowercase() ?: "libx264") + outputArguments += "-crf" to ffmpegOptions.constantRateFactor.get().let { "\"$it\"" } + outputArguments += "-b:v" to ffmpegOptions.videoBitrate.get().toString() + "K" + } + when (args.action) { Action.DOWNLOAD_DASH -> { + applyVideoArguments() outputArguments += "-ss" to "'${args.startTime}ms'" if (args.duration != null) { outputArguments += "-t" to "'${args.duration}ms'" } } Action.MERGE_OVERLAY -> { + applyVideoArguments() inputArguments += "-i" to args.overlay!!.absolutePath - outputArguments += "-filter_complex" to "\"[1:v][0:v]scale2ref=w=iw:h=ih[ovrl][main];[main][ovrl]overlay=(W-w)/2:(H-h)/2,scale=2*trunc(iw/2):2*trunc(ih/2)\"" + outputArguments += "-filter_complex" to "\"[0]scale2ref[img][vid];[img]setsar=1[img];[vid]nullsink;[img][1]overlay=(W-w)/2:(H-h)/2,scale=2*trunc(iw*sar/2):2*trunc(ih/2)\"" } Action.CONVERSION -> { if (ffmpegOptions.customAudioCodec.isEmpty()) { outputArguments -= "-c:a" } - outputArguments -= "-c:v" args.videoCodec?.let { + applyVideoArguments() + outputArguments -= "-c:v" outputArguments += "-c:v" to it } ?: run { outputArguments += "-vn" @@ -186,6 +192,7 @@ class FFMpegProcessor( } } Action.MERGE_MEDIA -> { + applyVideoArguments() inputArguments.clear() val filesInfo = args.inputs.mapNotNull { file -> runCatching { @@ -211,7 +218,7 @@ class FFMpegProcessor( filterSecondPart.append("[v$index][$index:a]") } else { containsNoSound = true - filterSecondPart.append("[v$index][${filesInfo.size}]") + filterSecondPart.append("[v$index][${filesInfo.size}:a]") } inputArguments += "-i" to file } @@ -228,9 +235,9 @@ class FFMpegProcessor( outputArguments += "-fps_mode" to "vfr" - outputArguments += "-filter_complex" to "\"$filterFirstPart ${filterSecondPart}concat=n=${filesInfo.size}:v=1:a=1[vout][aout]\"" - outputArguments += "-map" to "\"[aout]\"" - outputArguments += "-map" to "\"[vout]\"" + outputArguments += "-filter_complex" to "$filterFirstPart ${filterSecondPart}concat=n=${filesInfo.size}:v=1:a=1[vout][aout]" + outputArguments += "-map" to "[aout]" + outputArguments += "-map" to "[vout]" } finally { filesInfo.forEach { it.second.close() } } @@ -264,8 +271,8 @@ class FFMpegProcessor( filterParts.append("[a$index]") } filterParts.append("amix=inputs=${args.inputs.size}:duration=longest:normalize=0[aout]") - outputArguments += "-filter_complex" to "\"$filterParts\"" - outputArguments += "-map" to "\"[aout]\"" + outputArguments += "-filter_complex" to filterParts.toString() + outputArguments += "-map" to "[aout]" } } outputArguments += args.output.absolutePath diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/storage/AppDatabase.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/storage/AppDatabase.kt index 95f0b131..470c37b9 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/storage/AppDatabase.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/storage/AppDatabase.kt @@ -5,6 +5,8 @@ import me.eternal.purrfectsnap.RemoteSideContext import me.eternal.purrfectsnap.common.data.MessagingFriendInfo import me.eternal.purrfectsnap.common.data.MessagingGroupInfo import me.eternal.purrfectsnap.common.util.SQLiteDatabaseHelper +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -15,7 +17,11 @@ class AppDatabase( val executor: ExecutorService = Executors.newSingleThreadExecutor() lateinit var database: SQLiteDatabase - var receiveMessagingDataCallback: (friends: List, groups: List) -> Unit = { _, _ -> } + // Multi-subscriber event stream for messaging data updates + val messagingDataFlow = MutableSharedFlow, List>>( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) fun executeAsync(block: () -> Unit) { executor.execute { @@ -113,6 +119,22 @@ class AppDatabase( "id CHAR(36) PRIMARY KEY", "content TEXT", ), + "assistant_registry" to listOf( + "id VARCHAR PRIMARY KEY", + "kind VARCHAR", + "title VARCHAR", + "category VARCHAR", + "path TEXT", + "description TEXT", + "settingKey VARCHAR", + "screenRoute VARCHAR", + "allowedActions TEXT", + "allowedValues TEXT", + "aliases TEXT", + "commonTypos TEXT", + "examples TEXT", + "searchTokens TEXT", + ), )) } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/storage/AssistantRegistry.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/storage/AssistantRegistry.kt new file mode 100644 index 00000000..09ad8ad0 --- /dev/null +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/storage/AssistantRegistry.kt @@ -0,0 +1,93 @@ +package me.eternal.purrfectsnap.storage + +import android.content.ContentValues +import me.eternal.purrfectsnap.common.util.ktx.getStringOrNull +import org.json.JSONArray + +data class AssistantRegistryEntry( + val id: String, + val kind: String, + val title: String, + val category: String, + val path: String, + val description: String, + val settingKey: String? = null, + val screenRoute: String? = null, + val allowedActions: List = emptyList(), + val allowedValues: List = emptyList(), + val aliases: List = emptyList(), + val commonTypos: List = emptyList(), + val examples: List = emptyList(), + val searchTokens: List = emptyList() +) + +private fun List.toJsonArrayString(): String = JSONArray(this).toString() + +private fun parseStringList(raw: String?): List { + if (raw.isNullOrBlank()) return emptyList() + return runCatching { + val array = JSONArray(raw) + buildList { + for (index in 0 until array.length()) { + array.optString(index).takeIf { it.isNotBlank() }?.let(::add) + } + } + }.getOrDefault(emptyList()) +} + +fun AppDatabase.replaceAssistantRegistry(entries: List) { + database.beginTransaction() + try { + database.execSQL("DELETE FROM assistant_registry") + entries.forEach { entry -> + database.insert( + "assistant_registry", + null, + ContentValues().apply { + put("id", entry.id) + put("kind", entry.kind) + put("title", entry.title) + put("category", entry.category) + put("path", entry.path) + put("description", entry.description) + put("settingKey", entry.settingKey) + put("screenRoute", entry.screenRoute) + put("allowedActions", entry.allowedActions.toJsonArrayString()) + put("allowedValues", entry.allowedValues.toJsonArrayString()) + put("aliases", entry.aliases.toJsonArrayString()) + put("commonTypos", entry.commonTypos.toJsonArrayString()) + put("examples", entry.examples.toJsonArrayString()) + put("searchTokens", entry.searchTokens.toJsonArrayString()) + } + ) + } + database.setTransactionSuccessful() + } finally { + database.endTransaction() + } +} + +fun AppDatabase.getAssistantRegistryEntries(): List { + return database.rawQuery("SELECT * FROM assistant_registry", null).use { cursor -> + val entries = mutableListOf() + while (cursor.moveToNext()) { + entries += AssistantRegistryEntry( + id = cursor.getStringOrNull("id") ?: continue, + kind = cursor.getStringOrNull("kind") ?: "feature", + title = cursor.getStringOrNull("title") ?: "", + category = cursor.getStringOrNull("category") ?: "", + path = cursor.getStringOrNull("path") ?: "", + description = cursor.getStringOrNull("description") ?: "", + settingKey = cursor.getStringOrNull("settingKey"), + screenRoute = cursor.getStringOrNull("screenRoute"), + allowedActions = parseStringList(cursor.getStringOrNull("allowedActions")), + allowedValues = parseStringList(cursor.getStringOrNull("allowedValues")), + aliases = parseStringList(cursor.getStringOrNull("aliases")), + commonTypos = parseStringList(cursor.getStringOrNull("commonTypos")), + examples = parseStringList(cursor.getStringOrNull("examples")), + searchTokens = parseStringList(cursor.getStringOrNull("searchTokens")) + ) + } + entries + } +} diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/storage/Messaging.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/storage/Messaging.kt index 703a548f..7d0a6e2d 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/storage/Messaging.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/storage/Messaging.kt @@ -97,15 +97,16 @@ fun AppDatabase.replaceMessagingData( database.beginTransaction() try { friends.forEach { friend -> + // Industrial Filter: Only update existing friends, never auto-insert new ones. database.execSQL( - "INSERT OR REPLACE INTO friends (userId, dmConversationId, displayName, mutableUsername, bitmojiId, selfieId) VALUES (?, ?, ?, ?, ?, ?)", + "UPDATE friends SET dmConversationId = ?, displayName = ?, mutableUsername = ?, bitmojiId = ?, selfieId = ? WHERE userId = ?", arrayOf( - friend.userId, friend.dmConversationId, friend.displayName, friend.mutableUsername, friend.bitmojiId, - friend.selfieId + friend.selfieId, + friend.userId ) ) @@ -124,12 +125,13 @@ fun AppDatabase.replaceMessagingData( } groups.forEach { group -> + // Industrial Filter: Only update existing groups. database.execSQL( - "INSERT OR REPLACE INTO groups (conversationId, name, participantsCount) VALUES (?, ?, ?)", + "UPDATE groups SET name = ?, participantsCount = ? WHERE conversationId = ?", arrayOf( - group.conversationId, group.name, - group.participantsCount + group.participantsCount, + group.conversationId ) ) } @@ -139,10 +141,8 @@ fun AppDatabase.replaceMessagingData( database.endTransaction() } - // Notify with the full updated list from the DB - val allFriends = getFriends(descOrder = true) - val allGroups = getGroups() - receiveMessagingDataCallback(allFriends, allGroups) + // Notify all observers with the raw sync data (AddFriendDialog needs this) + messagingDataFlow.tryEmit(friends to groups) } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/AI.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/AI.kt new file mode 100644 index 00000000..ae1e8ad0 --- /dev/null +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/AI.kt @@ -0,0 +1,2420 @@ +package me.eternal.purrfectsnap.ui.manager + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Send +import androidx.compose.material.icons.filled.SmartToy +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import android.net.Uri +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.eternal.purrfectsnap.RemoteSideContext +import me.eternal.purrfectsnap.action.EnumQuickActions +import me.eternal.purrfectsnap.common.action.EnumAction +import me.eternal.purrfectsnap.common.config.ConfigContainer +import me.eternal.purrfectsnap.common.config.ConfigFlag +import me.eternal.purrfectsnap.common.config.DataProcessors +import me.eternal.purrfectsnap.common.config.PropertyKey +import me.eternal.purrfectsnap.common.config.PropertyValue +import me.eternal.purrfectsnap.common.data.TrackerEventType +import me.eternal.purrfectsnap.common.data.TrackerRuleAction +import me.eternal.purrfectsnap.common.data.TrackerRuleActionParams +import me.eternal.purrfectsnap.common.data.TrackerScopeType +import me.eternal.purrfectsnap.storage.addOrUpdateTrackerRuleEvent +import me.eternal.purrfectsnap.storage.getAssistantRegistryEntries +import me.eternal.purrfectsnap.storage.getFriends +import me.eternal.purrfectsnap.storage.getGroups +import me.eternal.purrfectsnap.storage.getTrackerRuleByName +import me.eternal.purrfectsnap.storage.newTrackerRule +import me.eternal.purrfectsnap.storage.replaceAssistantRegistry +import me.eternal.purrfectsnap.storage.setRuleTrackerScopes +import me.eternal.purrfectsnap.storage.setTrackerRuleState +import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette +import me.eternal.purrfectsnap.ui.util.saveFile +import kotlin.math.max +import kotlin.math.min + +private enum class AssistantRole { + USER, + ASSISTANT +} + +private data class AssistantMessage( + val role: AssistantRole, + val text: String +) + +private data class AssistantResult( + val reply: String, + val execute: (() -> Unit)? = null +) + +private data class AssistantFeature( + val id: String, + val name: String, + val description: String, + val path: String, + val searchPhrases: List, + val searchTokens: List, + val container: ConfigContainer? = null, + val propertyKey: PropertyKey<*>? = null, + val propertyValue: PropertyValue<*>? = null +) { + val isContainerToggle get() = container != null && propertyKey != null && container.hasGlobalState + val isProperty get() = propertyKey != null && propertyValue != null && propertyKey.dataType.type != DataProcessors.Type.CONTAINER +} + +private data class AssistantRoute( + val id: String, + val name: String, + val description: String, + val searchPhrases: List, + val searchTokens: List, + val navigate: () -> Unit +) + +private data class AssistantAction( + val id: String, + val name: String, + val searchPhrases: List, + val searchTokens: List, + val execute: () -> Unit +) + +private data class AssistantCandidate( + val id: String, + val kind: String, + val title: String, + val summary: String, + val execute: (() -> Unit)? = null +) + +private data class ScopeTarget( + val id: String, + val displayName: String +) + +private const val UNIQUE_OPTION_AMBIGUOUS = "__ambiguous__" +private const val TRAINING_LOG_URI_PREF = "assistant_training_log_uri" + +enum class ManagerAssistantTriggerStyle { + DEFAULT, + APHELION +} + +private class ManagerAssistantEngine( + private val context: RemoteSideContext, + private val routes: Routes +) { + private val logTag = "PurrfectSnapAI" + private val unsupportedPhrases = listOf( + "snapchat plus", + "snap plus", + "snapchat premium" + ) + private val stopWords = setOf( + "a", "an", "the", "to", "for", "on", "off", "of", "in", "it", "me", "please", + "set", "turn", "enable", "disable", "open", "go", "show", "what", "does", "do", + "is", "my", "can", "you", "make", "create", "add", "called", "named", "and", + "with", "into", "app", "assistant", "value", "feature" + ) + private val featureCatalog by lazy { buildFeatureCatalog() } + private val routeCatalog by lazy { buildRouteCatalog() } + private val actionCatalog by lazy { buildActionCatalog() } + @Volatile + private var registryPrimed = false + + suspend fun handle(query: String): AssistantResult { + val normalized = normalize(query) + if (normalized.isBlank()) { + return AssistantResult("Ask about a feature, tell me to change a setting, open a section, or create a friend tracker rule.") + } + context.log.info("AI handle query=\"$query\"", logTag) + capabilityReply(normalized)?.let { return it } + return directIntentReply(query, normalized) + ?: strictFallbackReply(normalized) + } + + private fun directIntentReply(rawQuery: String, normalized: String): AssistantResult? { + supportFlowReply(normalized)?.let { return it } + trackerRuleReply(rawQuery, normalized)?.let { return it } + featureMutationReply(rawQuery, normalized)?.let { return it } + actionReply(normalized)?.let { return it } + navigationReply(normalized)?.let { return it } + featureExplanationReply(normalized)?.let { return it } + return unsupportedReply(normalized) + } + + private fun supportFlowReply(normalized: String): AssistantResult? { + basicsHelpReply(normalized)?.let { return it } + messageLoggerHelpReply(normalized)?.let { return it } + downloaderHelpReply(normalized)?.let { return it } + galleryOverrideHelpReply(normalized)?.let { return it } + performanceModeHelpReply(normalized)?.let { return it } + customEmojiHelpReply(normalized)?.let { return it } + bridgeHelpReply(normalized)?.let { return it } + uiHelpReply(normalized)?.let { return it } + privacyHelpReply(normalized)?.let { return it } + autoOpenAutoSaveHelpReply(normalized)?.let { return it } + rulesHelpReply(normalized)?.let { return it } + friendTrackerHelpReply(normalized)?.let { return it } + streaksHelpReply(normalized)?.let { return it } + experimentalHelpReply(normalized)?.let { return it } + convertMessageHelpReply(normalized)?.let { return it } + scriptingHelpReply(normalized)?.let { return it } + crashAndDebugHelpReply(normalized)?.let { return it } + fakeSnapReply(normalized)?.let { return it } + latestSnapReply(normalized)?.let { return it } + installIssueReply(normalized)?.let { return it } + installReply(normalized)?.let { return it } + compatibilityReply(normalized)?.let { return it } + latestUpdatesReply(normalized)?.let { return it } + loginHelpReply(normalized)?.let { return it } + updateReply(normalized)?.let { return it } + deviceBanReply(normalized)?.let { return it } + configImportExportReply(normalized)?.let { return it } + currentThemeReply(normalized)?.let { return it } + developerReply(normalized)?.let { return it } + featureCountReply(normalized)?.let { return it } + permissionsReply(normalized)?.let { return it } + communityReply(normalized)?.let { return it } + githubReply(normalized)?.let { return it } + iosEmojiReply(normalized)?.let { return it } + profitReply(normalized)?.let { return it } + deletedContentReply(normalized)?.let { return it } + snapscoreReply(normalized)?.let { return it } + platformReply(normalized)?.let { return it } + installIssueReply(normalized)?.let { return it } + notOpeningAfterReinstallReply(normalized)?.let { return it } + continuousSnapReply(normalized)?.let { return it } + performanceReply(normalized)?.let { return it } + updateCadenceReply(normalized)?.let { return it } + crashLagReply(normalized)?.let { return it } + mapperReply(normalized)?.let { return it } + rootedLsposedReply(normalized)?.let { return it } + trackerSupportReply(normalized)?.let { return it } + snapchatMenuReply(normalized)?.let { return it } + crashLogsReply(normalized)?.let { return it } + safetyReply(normalized)?.let { return it } + duplicateMessagesReply(normalized)?.let { return it } + bugReportReply(normalized)?.let { return it } + bestFeaturesReply(normalized)?.let { return it } + downloadSnapsReply(normalized)?.let { return it } + hideTypingIndicatorReply(normalized)?.let { return it } + screenshotIndicatorReply(normalized)?.let { return it } + return null + } + + private fun basicsHelpReply(normalized: String): AssistantResult? { + if (normalized == "hi" || normalized == "hello" || normalized == "hey") { + return AssistantResult("Hi, I am PurrfectSnap AI, an AI assistant designed to help you with anything related to PurrfectSnap and also help our developers and contributors, who deserve a little rest :)") + } + if (normalized.contains("what is purrfectsnap used for")) { + return AssistantResult("PurrfectSnap enhances Snapchat with privacy tools, downloader features, automation, UI tweaks, tracking tools, and quality-of-life features.") + } + if (normalized.contains("enable purrfectsnap for snapchat") || normalized.contains("how do i enable purrfectsnap")) { + return AssistantResult("Install PurrfectSnap and follow the on-screen instructions. It will set everything up for you.") + } + if (normalized.contains("know if purrfectsnap is working")) { + return AssistantResult("Open Snapchat and check whether your enabled features appear or work. If they do, PurrfectSnap is active.") + } + if (normalized.contains("where can i find the features screen")) { + return AssistantResult("Open PurrfectSnap and tap the Features tab in the bottom bar.") + } + if (normalized.contains("reload purrfectsnap settings") || normalized.contains("restart snapchat after changing")) { + return AssistantResult("After changing important settings, force stop and reopen Snapchat so the hooks reload cleanly.") + } + if ((normalized.contains("features") && normalized.contains("not working")) || normalized.contains("none of the features work")) { + return AssistantResult("Try reinstalling everything.") + } + if (normalized.contains("check my purrfectsnap version")) { + return AssistantResult("Open the homepage of PurrfectSnap. Your current version is shown there.") + } + if (normalized.contains("global features and per user")) { + return AssistantResult("Global features affect Snapchat everywhere. Per-user features or rules affect only the selected friend or chat.") + } + if (normalized.contains("disable a broken feature safely")) { + return AssistantResult("Disable the last feature you changed, force stop Snapchat, and reopen it. If needed, disable features one by one until the issue stops.") + } + if (normalized.contains("reset all purrfectsnap settings")) { + return AssistantResult("Open Settings in PurrfectSnap and use the reset option there to restore defaults.") + } + if (normalized.contains("backup my purrfectsnap configuration")) { + return AssistantResult("Go to the Features tab, tap the three dots in the top-right corner, then select Export Config.") + } + if (normalized.contains("restore my purrfectsnap settings")) { + return AssistantResult("Go to the Features tab, tap the three dots in the top-right corner, then select Import Config.") + } + if (normalized.contains("safe to enable together")) { + return AssistantResult("Most features are fine together, but test heavier features like Performance Mode, Friend Tracker, Custom Emoji, and experimental features one at a time.") + } + if (normalized.contains("one phone but not another") || normalized.contains("work differently on lspatch")) { + return AssistantResult("Some features depend on device firmware, Android version, root environment, and how Snapchat is patched. LSPatch and LSPosed can behave differently.") + } + if (normalized.contains("need native hooks")) { + return AssistantResult("Some features need native hooks because they patch Snapchat code paths that Java-only hooks cannot fully control.") + } + return null + } + + private fun messageLoggerHelpReply(normalized: String): AssistantResult? { + if (!(normalized.contains("message logger") || normalized.contains("logged messages") || normalized.contains("logged media"))) return null + return when { + normalized.contains("enable") -> AssistantResult("Open Features, search Message Logger, and enable it.") + normalized.contains("where is") || normalized.contains("located") -> AssistantResult("Message Logger is in the Messaging section.") + normalized.contains("what does") -> AssistantResult("Message Logger records chat activity so you can review messages, including deleted ones when available.") + normalized.contains("view") || normalized.contains("see deleted") -> AssistantResult("Open Logs or the logger-related screens in PurrfectSnap to review recorded messages and deleted-message entries.") + normalized.contains("blacklist") || normalized.contains("whitelist") -> AssistantResult("Blacklist mode excludes selected users, while whitelist mode logs only selected users.") + normalized.contains("exclude one user") || normalized.contains("only from selected users") -> AssistantResult("Use the per-user controls or rules to include or exclude specific users from Message Logger.") + normalized.contains("export") && normalized.contains("database") -> AssistantResult("Use database export for backups, HTML for browsing, and TXT for simple readable exports.") + normalized.contains("html") -> AssistantResult("Use HTML export if you want a readable chat-style export.") + normalized.contains("txt") -> AssistantResult("Use TXT export if you want a simple text export. Random IDs can appear depending on what metadata Snapchat exposes.") + normalized.contains("import") -> AssistantResult("Import the Message Logger backup database through the relevant import option, then reopen Snapchat and verify the data appears.") + normalized.contains("clear") -> AssistantResult("Clear Message Logger data from the relevant logger or logs screen in PurrfectSnap.") + normalized.contains("download") && normalized.contains("deleted") -> AssistantResult("Deleted logged media may not always have an attachment available. If Snapchat no longer exposes the attachment, Download can show No attachment found.") + else -> AssistantResult("Message Logger can record, filter, export, import, and review logged chat activity depending on the available Snapchat data.") + } + } + + private fun downloaderHelpReply(normalized: String): AssistantResult? { + val downloaderRelated = normalized.contains("download") || normalized.contains("downloader") || normalized.contains("ffmpeg") || normalized.contains("overlay") + if (!downloaderRelated) return null + return when { + normalized.contains("enable media downloader") || normalized.contains("download context menu") -> AssistantResult("Open Features, go to Downloader, and enable Download Context Menu or the downloader options you want.") + normalized.contains("story") -> AssistantResult("Use the downloader features or context menus available on stories where PurrfectSnap exposes them.") + normalized.contains("profile picture") -> AssistantResult("Enable Download Profile Pictures in Downloader.") + normalized.contains("voice note") -> AssistantResult("Voice note downloading depends on the media attachment being available through Snapchat. Use the downloader/context menu where supported.") + normalized.contains("group chat") || normalized.contains("saved chat media") || normalized.contains("unsaveable media") -> AssistantResult("Use the download context menu on the media when available. Support can vary depending on how Snapchat exposes that media.") + normalized.contains("where are downloaded") || normalized.contains("folder path") -> AssistantResult("Downloaded Snapchat files are saved to your configured download folder, and you can change that folder path in the downloader settings.") + normalized.contains("organize") && normalized.contains("username") -> AssistantResult("Use the downloader naming and folder options to organize files by username.") + normalized.contains("organize") && normalized.contains("date") -> AssistantResult("Use the downloader naming and folder options if you want files organized by date.") + normalized.contains("duplicate") -> AssistantResult("Allow Duplicate lets the same media be downloaded multiple times. Disable it if you want duplicate prevention.") + normalized.contains("overlay") -> AssistantResult("Merge Overlays combines text and media overlays into the downloaded output.") + normalized.contains("automatic media download") || normalized.contains("auto download") -> AssistantResult("Enable Auto Download in Downloader, then use per-user or rules-based controls if you want only selected users.") + normalized.contains("download logging") || normalized.contains("download history") -> AssistantResult("Use the downloader logs/history screens if enabled to review past download activity.") + normalized.contains("missing") || normalized.contains("not playing") || normalized.contains("fail") -> AssistantResult("Download failures usually come from unavailable attachments, unsupported media types, or post-processing issues such as FFmpeg processing.") + normalized.contains("ffmpeg") -> AssistantResult("FFmpeg processing helps post-process downloaded media and can improve compatibility for certain outputs.") + normalized.contains("quality") -> AssistantResult("Use the downloader and global quality settings to control output quality where supported.") + else -> AssistantResult("The Downloader covers context-menu downloads, auto-download, profile pictures, overlays, FFmpeg processing, and folder organization.") + } + } + + private fun galleryOverrideHelpReply(normalized: String): AssistantResult? { + if (!(normalized.contains("gallery media send override") || normalized.contains("override mode") || normalized.contains("gallery media") || normalized.contains("send override"))) return null + return when { + normalized.contains("where is") || normalized.contains("located") -> AssistantResult("Gallery Media Send Override is in the Messaging section.") + normalized.contains("enable") -> AssistantResult("Open Features, go to Messaging, then enable Gallery Media Send Override.") + normalized.contains("disabled mode") -> AssistantResult("Disabled mode keeps Snapchat's normal behavior.") + normalized.contains("always ask") -> AssistantResult("Always Ask lets you choose the send type each time.") + normalized.contains("snap mode") -> AssistantResult("Snap mode tries to send gallery media as a normal snap.") + normalized.contains("always note") || normalized.contains("note mode") -> AssistantResult("Always Note sends the selected gallery media as a note.") + normalized.contains("saveable snap") -> AssistantResult("Saveable Snap mode sends the media in a way that can be saved in chat.") + normalized.contains("split") || normalized.contains("duration") || normalized.contains("video") -> AssistantResult("If Snapchat still splits long gallery videos or resets duration, force stop Snapchat and retry. Some resend/queue cases depend on Snapchat's own media handling.") + normalized.contains("troubleshoot") || normalized.contains("not working") -> AssistantResult("If Gallery Media Send Override is not working, make sure Media File Picker is enabled, then force stop and reopen Snapchat.") + else -> AssistantResult("Gallery Media Send Override controls how gallery media is sent: disabled, always ask, snap, note, or saveable snap.") + } + } + + private fun performanceModeHelpReply(normalized: String): AssistantResult? { + if (!normalized.contains("performance mode")) return null + return when { + normalized.contains("where") -> AssistantResult("Performance Mode is in Global.") + normalized.contains("difference") -> AssistantResult("Disabled keeps normal behavior, Smooth is lighter optimization, and Max is the most aggressive profile.") + normalized.contains("enable smooth") -> AssistantResult("Set Performance Mode to Smooth in Global.") + normalized.contains("enable max") -> AssistantResult("Set Performance Mode to Max in Global.") + normalized.contains("disable") || normalized.contains("reset") -> AssistantResult("Set Performance Mode back to Disabled to restore the default profile.") + normalized.contains("freeze") || normalized.contains("blank") || normalized.contains("glitch") || normalized.contains("missing ui") -> AssistantResult("If Performance Mode causes freezes, blank screens, or UI glitches, disable it or switch from Max to Smooth and reopen Snapchat.") + normalized.contains("safest") -> AssistantResult("Smooth is the safer option. Max is more aggressive and may break on some devices.") + normalized.contains("oneplus") -> AssistantResult("On some OnePlus devices, Max Performance Mode can be unstable. Smooth or Disabled is safer there.") + normalized.contains("samsung") -> AssistantResult("Samsung devices can behave differently from OnePlus because firmware and graphics handling differ.") + normalized.contains("cpu") -> AssistantResult("Performance Mode can increase CPU usage on some devices, especially Max.") + else -> AssistantResult("Performance Mode changes Snapchat responsiveness and loading behavior. If it causes issues, lower it or disable it.") + } + } + + private fun customEmojiHelpReply(normalized: String): AssistantResult? { + if (!(normalized.contains("custom emoji") || normalized.contains("emoji font") || normalized.contains("ios emoji"))) return null + return when { + normalized.contains("where") -> AssistantResult("Custom Emoji is in the Features tab. Search for Custom Emoji to configure it.") + normalized.contains("enable") || normalized.contains("ios emoji") -> iosEmojiReply(normalized) + ?: AssistantResult("Download the TTF file, import it through File Imports, then select it in Custom Emoji and reopen Snapchat.") + normalized.contains("font path") || normalized.contains("custom_emoji_font_path") -> AssistantResult("That setting stores the selected emoji font file path.") + normalized.contains("lag") || normalized.contains("stutter") || normalized.contains("crash") || normalized.contains("sigsegv") -> AssistantResult("If Custom Emoji causes lag or crashes, disable it first. Compatibility differs across devices and patch environments.") + normalized.contains("lsposed") || normalized.contains("lspatch") -> AssistantResult("Custom Emoji can behave differently on LSPosed and LSPatch. If it is unstable, disable it.") + normalized.contains("disable") || normalized.contains("recover") -> AssistantResult("If Snapchat keeps crashing after enabling Custom Emoji, disable it from PurrfectSnap and reopen Snapchat.") + else -> AssistantResult("Custom Emoji lets you load a custom emoji font, but support varies across devices and environments.") + } + } + + private fun bridgeHelpReply(normalized: String): AssistantResult? { + if (!(normalized.contains("bridge") || normalized.contains("deadobjectexception") || normalized.contains("binder death"))) return null + return AssistantResult("The PurrfectSnap bridge is the helper connection used for manager and hook communication. If bridge-related crashes happen, collect logs and report them because they are runtime stability issues.") + } + + private fun uiHelpReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("amoled") || normalized.contains("theme") || normalized.contains("ui ") || + normalized.contains("spotlight") || normalized.contains("discover") || normalized.contains("friend feed entry") || + normalized.contains("settings gear") || normalized.contains("vertical story") + if (!relevant) return null + return when { + normalized.contains("amoled") || normalized.contains("change the theme") -> AssistantResult("Use Manager Theme in Global to switch between Legacy and Aphelion.") + normalized.contains("spotlight") || normalized.contains("discover") || normalized.contains("hide unwanted") -> AssistantResult("Use the relevant UI and global story/tab controls to hide unwanted Snapchat surfaces.") + normalized.contains("friend feed entry") || normalized.contains("hide a specific user") -> AssistantResult("Hide Friend Feed Entry can hide selected users from parts of the feed, but Snapchat surfaces can behave differently, so test and refresh after changing it.") + normalized.contains("settings gear") -> AssistantResult("If the settings gear is missing, ensure the relevant injector option is enabled and reopen Snapchat.") + normalized.contains("blank") || normalized.contains("corruption") -> AssistantResult("If a UI tweak causes blank screens or corruption, disable the last UI-related feature you enabled and reopen Snapchat.") + else -> AssistantResult("PurrfectSnap UI features let you change the manager theme, hide Snapchat surfaces, and tweak message and story presentation.") + } + } + + private fun privacyHelpReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("stealth mode") || normalized.contains("typing indicator") || normalized.contains("auto read") || + normalized.contains("read receipt") || normalized.contains("anonymous story") || normalized.contains("unlimited conversation pinning") || + normalized.contains("e2e encryption") || normalized.contains("privacy features") + if (!relevant) return null + return when { + normalized.contains("stealth mode") -> AssistantResult("Chat Stealth and related privacy tools help hide activity like typing or presence depending on the feature enabled.") + normalized.contains("typing indicator") -> AssistantResult("Use Hide Typing Notifications to hide your typing indicator.") + normalized.contains("auto read") || normalized.contains("read receipt") -> AssistantResult("Auto Read and related privacy options can affect read behavior per user or globally depending on your setup.") + normalized.contains("anonymous story") -> AssistantResult("Anonymous story viewing helps reduce viewing indicators where supported.") + normalized.contains("pin") -> AssistantResult("Unlimited Conversation Pinning lets you pin more conversations locally than Snapchat normally allows.") + normalized.contains("e2e encryption") -> AssistantResult("Use E2E Encryption where you want encrypted chat behavior supported by Snapchat and PurrfectSnap.") + else -> AssistantResult("PurrfectSnap privacy features include stealth tools, typing hiding, screenshot bypass, anonymous story viewing, and local conversation controls.") + } + } + + private fun autoOpenAutoSaveHelpReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("auto open") || normalized.contains("auto save") || normalized.contains("snap pre fetch") || normalized.contains("prefetch") + if (!relevant) return null + return when { + normalized.contains("auto open") && normalized.contains("specific user") -> AssistantResult("Use per-user options or rules if you want Auto Open for only selected users.") + normalized.contains("exclude") -> AssistantResult("Use the per-user or rules-based exclude options for Auto Open or Auto Save.") + normalized.contains("drain battery") || normalized.contains("overheat") -> AssistantResult("Auto Open and prefetch can increase battery use and heat. Use thermal protection and disable aggressive settings if needed.") + normalized.contains("not work") || normalized.contains("stuck") || normalized.contains("loading forever") -> AssistantResult("If Auto Open or Auto Save gets stuck, reduce aggressive settings, reopen Snapchat, and test whether prefetch or heavy background work is causing it.") + normalized.contains("what does") -> AssistantResult("Snap Pre-Fetch caches incoming snap media early, while Auto Open and Auto Save automate opening or saving where supported.") + else -> AssistantResult("Auto Open, Auto Save, and Snap Pre-Fetch can be configured globally, per user, or through rules.") + } + } + + private fun rulesHelpReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("rules engine") || normalized.contains("message logger rule") || normalized.contains("auto download rule") || + normalized.contains("auto save rule") || normalized.contains("auto open rule") || normalized.contains("whitelist mode") || + normalized.contains("blacklist mode") || normalized.contains("which takes priority") + if (!relevant) return null + return AssistantResult("Rules let you apply Message Logger, Auto Download, Auto Save, and Auto Open behavior to selected users or groups. Blacklist excludes selected targets, whitelist limits the feature to selected targets, and per-user settings can interact with global defaults.") + } + + private fun friendTrackerHelpReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("friend tracker") || normalized.contains("track snapchat profile changes") || normalized.contains("display name changes") || normalized.contains("bitmoji changes") + if (!relevant) return null + return when { + normalized.contains("how to use") || normalized.contains("how do i use") || normalized.contains("set up") -> + AssistantResult("Open the Friend Tracker tab, enable Friend Tracker, then create rules for events like typing, screenshot, or message read.") + normalized.contains("enable") -> AssistantResult("Enable Friend Tracker, then create rules or review its logs for the events you want.") + normalized.contains("export") || normalized.contains("purge") || normalized.contains("storage") -> AssistantResult("Friend Tracker stores activity data until you export or purge it from its management screens.") + normalized.contains("background") -> AssistantResult("Disable the background-related tracking options if you do not want Friend Tracker running in the background.") + normalized.contains("display name") || normalized.contains("bitmoji") || normalized.contains("profile changes") -> AssistantResult("Friend Tracker can help monitor profile-related changes, depending on the available event coverage.") + normalized.contains("disable tracking for one friend") -> AssistantResult("Use Friend Tracker scope or per-user controls to exclude that friend.") + else -> AssistantResult("Friend Tracker records supported Snapchat activity and profile-related events so you can review them or build rules around them.") + } + } + + private fun streaksHelpReply(normalized: String): AssistantResult? { + if (!normalized.contains("streak")) return null + return when { + normalized.contains("enable") -> AssistantResult("Open Streaks Reminder and enable it there.") + normalized.contains("interval") || normalized.contains("remaining time") -> AssistantResult("Use the Interval and Remaining Time options inside Streaks Reminder.") + normalized.contains("group streak") -> AssistantResult("Enable the relevant streak reminder options if you want group-related reminders where supported.") + normalized.contains("not getting") || normalized.contains("duplicate") -> AssistantResult("Check your Streaks Reminder settings and notification behavior if reminders are missing or duplicated.") + normalized.contains("customize") || normalized.contains("stop") -> AssistantResult("Use the Streaks Reminder settings to tune or disable the reminders.") + else -> AssistantResult("Streaks Reminder periodically reminds you about streak activity based on the configured timing options.") + } + } + + private fun experimentalHelpReply(normalized: String): AssistantResult? { + if (!(normalized.contains("experimental") || normalized.contains("native hooks") || normalized.contains("spoofing") || normalized.contains("account switcher") || normalized.contains("app lock") || normalized.contains("better transcript") || normalized.contains("cof"))) return null + return AssistantResult("Experimental features are advanced options that can be powerful but less stable. Test them one at a time, and disable the last one you changed if Snapchat starts crashing.") + } + + private fun convertMessageHelpReply(normalized: String): AssistantResult? { + if (!normalized.contains("convert message locally") && !normalized.contains("convert a sent snap")) return null + return AssistantResult("Convert Message Locally turns snaps into chat-style external media locally where supported. If the converted media disappears or won’t open, Snapchat likely refreshed or no longer exposes it the same way.") + } + + private fun scriptingHelpReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("script") || normalized.contains("scripting") || normalized.contains("developer mode") || normalized.contains("module folder") + if (!relevant) return null + return when { + normalized.contains("open the scripting ui") -> AssistantResult("Open the Scripts tab to access the scripting UI.") + normalized.contains("developer mode") -> AssistantResult("Enable Developer Mode from the scripting-related settings if you need advanced script behavior.") + normalized.contains("module folder") -> AssistantResult("The module folder is where your scripts are stored.") + normalized.contains("auto reload") -> AssistantResult("Enable Auto Reload in Scripting if you want scripts to reload when they change.") + normalized.contains("disable a broken script") || normalized.contains("view script logs") -> AssistantResult("Open the Scripts tab to disable the broken script and review any related script logs.") + normalized.contains("reset the scripting environment") -> AssistantResult("Use the scripting UI and related settings to reset or reload the scripting environment.") + else -> AssistantResult("Scripting lets you extend PurrfectSnap with custom scripts and developer tools.") + } + } + + private fun crashAndDebugHelpReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("crash") || normalized.contains("freeze") || normalized.contains("debug") || normalized.contains("sigsegv") || normalized.contains("logs should i provide") + if (!relevant) return null + return when { + normalized.contains("collect purrfectsnap logs") || normalized.contains("what logs should i provide") || normalized.contains("sigsegv") || normalized.contains("native or java") -> crashLogsReply(normalized) + normalized.contains("share device info") -> AssistantResult("Share your device model, Android version, patch environment, and the relevant crash logs when reporting debugging issues.") + normalized.contains("which feature caused a crash") -> AssistantResult("Disable the last changed feature first, then test again. If needed, re-enable features one by one to find the culprit.") + normalized.contains("oneplus") || normalized.contains("lspatch") || normalized.contains("background") -> AssistantResult("Some crashes are device- or patch-environment-specific. Capture logs and note whether the issue happens only on OnePlus, LSPatch, or after background resume.") + else -> AssistantResult("If Snapchat crashes or freezes, collect logs and report the issue to the group with the relevant device and setup details.") + } + } + + private fun latestSnapReply(normalized: String): AssistantResult? { + val relevant = (normalized.contains("latest snap") || normalized.contains("latest snapchat") || + normalized.contains("latest update") || normalized.contains("newest update") || normalized.contains("which snapchat version")) && + (normalized.contains("work") || normalized.contains("support") || normalized.contains("compatible")) + if (!relevant) return null + return AssistantResult("PurrfectSnap automatically downloads the latest Snapchat version for you. You do not need to do anything manually.") + } + + private fun installReply(normalized: String): AssistantResult? { + val relevant = (normalized.contains("install") || normalized.contains("setup")) && + (normalized.contains("safe") || normalized.contains("safely") || normalized.contains("how do i")) + if (!relevant) return null + return AssistantResult("Just install PurrfectSnap and follow the on-screen instructions. It'll do the job for you.") + } + + private fun rootedLsposedReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("lsposed") || normalized.contains("xposed") + if (!relevant) return null + return AssistantResult("LSPosed is needed only for rooted devices.") + } + + private fun compatibilityReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("compatib") || normalized.contains("android version") || normalized.contains("supported android") + if (!relevant) return null + return AssistantResult("PurrfectSnap supports Android 11 and above.") + } + + private fun latestUpdatesReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("latest updates") || normalized.contains("get updates") || normalized.contains("new updates") + if (!relevant) return null + return AssistantResult("Yes, PurrfectSnap automatically downloads a latest version from a set of versions.") + } + + private fun loginHelpReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("login") || normalized.contains("log in") || normalized.contains("temporarily disabled") + if (!relevant) return null + if (normalized.contains("issue") || normalized.contains("can't") || normalized.contains("cannot") || normalized.contains("temporarily disabled") || normalized.contains("failed attempts")) { + return AssistantResult( + "Go to Snapchat and log in. If you face issues, tap the Can't Login button on the login screen and follow the temporarily disabled fix shown there." + ) + } + return AssistantResult("Go to Snapchat and log in. If you face issues, tap the Can't Login button on the login screen and follow the temporarily disabled fix shown there.") + } + + private fun updateReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("how do i update") || normalized.contains("how to update") || normalized.contains("update purrfectsnap") || normalized.contains("update snapchat") + if (!relevant) return null + return AssistantResult( + "To update Snapchat, use Reset and Restart PurrfectSnap in Settings and redo the process. You may get a newer randomized Snap version for security. To update PurrfectSnap itself, use the update button on the homepage when one is available." + ) + } + + private fun configImportExportReply(normalized: String): AssistantResult? { + if (normalized.contains("import config") || normalized.contains("import settings")) { + return AssistantResult("Go to the Features tab, tap the three dots in the top-right corner, then select Import Config.") + } + if (normalized.contains("export config") || normalized.contains("export settings")) { + return AssistantResult("Go to the Features tab, tap the three dots in the top-right corner, then select Export Config.") + } + return null + } + + private fun currentThemeReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("what is this theme") || normalized.contains("which theme") || normalized.contains("current theme") + if (!relevant) return null + val currentTheme = context.config.root.global.uiSettings.managerTheme.get() + return AssistantResult(if (currentTheme == "APHELION") "You are currently using the Aphelion theme." else "You are currently using the Legacy theme.") + } + + private fun developerReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("who develops") || normalized.contains("who made") || normalized.contains("who created purrfectsnap") + if (!relevant) return null + return AssistantResult("Eternal and his team founded the mod back in October 2025. Now, it's maintained by Kaladin, schrodingerspet, and their team.") + } + + private fun featureCountReply(normalized: String): AssistantResult? { + val relevant = (normalized.contains("how many") || normalized.contains("count")) && normalized.contains("feature") + if (!relevant) return null + return AssistantResult("PurrfectSnap currently has ${featureCatalog.size} assistant-indexed features.") + } + + private fun permissionsReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("permission") || normalized.contains("what does it require") + if (!relevant) return null + return AssistantResult("PurrfectSnap mainly needs internet, notifications, install packages, and display over other apps for the in-Snapchat menu. Biometric is optional for App Lock, and Shizuku is only needed on the non-root setup path.") + } + + private fun communityReply(normalized: String): AssistantResult? { + if (normalized.contains("human help") || normalized.contains("need help") || normalized.contains("telegram link")) { + return AssistantResult("Click on the Telegram button on the homepage for the channel link, then tap the pinned message in our channel where you will find the link to our group.") + } + if (normalized.contains("where is the telegram group") || normalized.contains("where is telegram group")) { + return AssistantResult("Click on the Telegram button on the homepage for the channel link, then tap the pinned message in our channel where you will find the link to our group.") + } + if (normalized.contains("active community") || normalized.contains("join the community")) { + return AssistantResult("Yes, this mod is actively maintained. To join the community, click on the Telegram button on the homepage.") + } + if (normalized.contains("where to report issue") || normalized.contains("where do i report") || normalized.contains("report issue")) { + return AssistantResult("Refer to our Telegram group if you need to report a bug or any question that I couldn't answer. Click on the Telegram button in the homepage and then tap on the pinned msg in our channel where you will find the link to our group.") + } + return null + } + + private fun githubReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("github link") || normalized.contains("where is github") + if (!relevant) return null + return AssistantResult("Go to the homepage of PurrfectSnap and tap on the GitHub button.") + } + + private fun iosEmojiReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("ios emoji") || normalized.contains("custom emoji") || normalized.contains("how to use emoji") || normalized.contains("change font") + if (!relevant) return null + return AssistantResult("Download the TTF file and import it through File Imports on the homepage of PurrfectSnap. Then go to the Features tab, search Custom Emoji, and select that file. Force stop and reopen Snapchat. If it still doesn't work, then it is not supported for your device.") + } + + private fun profitReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("earn profit") || normalized.contains("non profit") || normalized.contains("profit mod") + if (!relevant) return null + return AssistantResult("PurrfectSnap is completely free and is a non-profit mod.") + } + + private fun performanceReply(normalized: String): AssistantResult? { + if (normalized.contains("battery") && (normalized.contains("drain") || normalized.contains("more"))) { + return AssistantResult("It can drain battery slightly, especially if you use high-intensity features like Friend Tracker.") + } + if (normalized.contains("camera quality") || normalized.contains("camera performance")) { + return AssistantResult("No. In fact, it can improve it a lot through our Performance Mode feature, which is set to Max by default.") + } + return null + } + + private fun deletedContentReply(normalized: String): AssistantResult? { + if (normalized.contains("logs of deleted") || normalized.contains("deleted messages or snap")) { + return AssistantResult("Logger History in the homepage quick actions.") + } + if (normalized.contains("see deleted snaps") || normalized.contains("see deleted chats")) { + return AssistantResult("Yes, I have turned on the Message Logger feature for you, which will help you see deleted messages and chats.", execute = { + featureCatalog.firstOrNull { it.id == "message_logger" }?.container?.globalState = true + featureCatalog.firstOrNull { it.id == "auto_purge" }?.propertyValue?.let { + @Suppress("UNCHECKED_CAST") + (it as? PropertyValue)?.set("never") + } + context.config.writeConfig() + }) + } + return null + } + + private fun snapscoreReply(normalized: String): AssistantResult? { + if (normalized.contains("increase my snapscore") || normalized.contains("boost snapscore")) { + return AssistantResult("Join Snap Spam groups and use the Auto Open Snaps feature along with them. You can also use the continuous snap sender feature to take it up a notch.") + } + if (normalized.contains("spam groups")) { + return AssistantResult("Ask in the Telegram group.") + } + if (normalized.contains("spoof snapscore") || normalized.contains("fake snapscore")) { + return AssistantResult("Yes. Use the Snapscore spoof-related features in the spoofing section where available.") + } + return null + } + + private fun platformReply(normalized: String): AssistantResult? { + if (normalized.contains("ios version")) return AssistantResult("No.") + if ((normalized.contains("creating an account") || normalized.contains("create account")) && normalized.contains("error")) { + return AssistantResult("You can't create accounts while using PurrfectSnap. You have to create them on stock Snapchat, then log in using the mod.") + } + return null + } + + private fun installIssueReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("app not installed") || normalized.contains("package appears to be invalid") + if (!relevant) return null + return AssistantResult("You must have an existing Snapchat installed either in a work profile or as a cloned app. Uninstall it. If you already did that and still see the same issue, make sure you uninstalled it with the option to keep data unchecked. If not done, install Snapchat from the Play Store and then uninstall it like that. If it still does not work, you need a PC to uninstall Snapchat using ADB. Search on Google how to do that.") + } + + private fun notOpeningAfterReinstallReply(normalized: String): AssistantResult? { + val relevant = (normalized.contains("snapchat is not opening") || normalized.contains("snapchat not opening") || normalized.contains("still not opening")) && + (normalized.contains("reset") || normalized.contains("clean install") || normalized.contains("reinstall")) + if (!relevant) return null + return AssistantResult("Do not HMAL. Remove Snapchat and PurrfectSnap from HMAL. Also remove them from Denylist.") + } + + private fun continuousSnapReply(normalized: String): AssistantResult? { + if (!normalized.contains("continuous snap sender")) return null + return AssistantResult("Go to Snapchat, open a friend's chat, tap the gallery icon, choose a picture, hit send, then tap Send as Continuous Snap, enter the number, and hit send.", execute = { + featureCatalog.firstOrNull { it.id == "media_file_picker" }?.propertyValue?.let { + @Suppress("UNCHECKED_CAST") + (it as? PropertyValue)?.set(true) + } + featureCatalog.firstOrNull { it.id == "mode" && it.path.contains("Gallery", ignoreCase = true) }?.propertyValue?.let { + @Suppress("UNCHECKED_CAST") + (it as? PropertyValue)?.set("always_ask") + } + context.config.writeConfig() + }) + } + + private fun updateCadenceReply(normalized: String): AssistantResult? { + if (normalized.contains("actively maintained")) { + return AssistantResult("Yes, with weekly or monthly updates. Join the Telegram channel for up-to-date announcements or keep an eye on the Announcements tab.") + } + if (normalized.contains("next update")) { + return AssistantResult("Soon.") + } + val relevant = normalized.contains("how often is it updated") || normalized.contains("weekly or monthly") + if (!relevant) return null + return AssistantResult("PurrfectSnap is updated weekly or monthly, depending on what needs to be shipped.") + } + + private fun crashLagReply(normalized: String): AssistantResult? { + if ((normalized.contains("crash") || normalized.contains("crashes")) && !normalized.contains("log")) { + return AssistantResult("Sometimes, yes. It is an under-development project. If you face issues, report them to our group.") + } + if (normalized.contains("lag") || normalized.contains("freeze")) { + return AssistantResult("Sometimes, yes. It is an under-development project. If you face issues, report them to our group.") + } + return null + } + + private fun mapperReply(normalized: String): AssistantResult? { + val relevant = (normalized.contains("break when snap updates") || normalized.contains("stop working after update") || + normalized.contains("do features stop working") || normalized.contains("latest snap update")) && + (normalized.contains("update") || normalized.contains("snap")) + if (!relevant) return null + return AssistantResult("We have implemented a mapper which should cover changes to Snapchat's code in new updates.") + } + + private fun trackerSupportReply(normalized: String): AssistantResult? { + if (normalized.contains("sent only to you")) { + return AssistantResult("Yes, I have turned on the feature for you. It's located in Message Indicators.", execute = { + setBooleanFeatureEnabled( + ids = listOf("message_indicators"), + phraseHints = listOf("Message Indicators", "sent only to you"), + enabled = true + ) + }) + } + if (normalized.contains("i can see you") && (normalized.contains("disable") || normalized.contains("turn off"))) { + return AssistantResult("Open Friend Tracker and disable the I Can See You rules there.") + } + if (normalized.contains("stories") && normalized.contains("typing")) { + return AssistantResult("Use Hide Typing Notifications for typing privacy, and use the downloader or story-related tools separately for stories.") + } + return null + } + + private fun fakeSnapReply(normalized: String): AssistantResult? { + val asksPossibility = listOf("is it possible", "can i", "can you", "possible").any { normalized.contains(it) } + val relevant = listOf( + "media upload tag", "fake snap", "fake snaps", "send from gallery", "gallery snap", + "gallery media", "remove upload tag", "upload tag", "without tag", "without label" + ).any { normalized.contains(it) } + if (!relevant) return null + if (asksPossibility && !listOf("how", "guide", "enable", "setup", "set up").any { normalized.contains(it) }) { + return AssistantResult("Yes, it's possible. Do you want me to guide you?") + } + return AssistantResult( + reply = "I have enabled the necessary settings for this feature. Now force stop and reopen Snapchat, send a picture from your gallery, select your friend, and tap Send. A dialog will appear; tap Make Snap Saveable in Chat and then tap Send!", + execute = { + val mediaFilePicker = featureCatalog.firstOrNull { it.id == "media_file_picker" } + val overrideMode = featureCatalog.firstOrNull { it.id == "mode" && it.path.contains("Gallery", ignoreCase = true) } + mediaFilePicker?.propertyValue?.let { + @Suppress("UNCHECKED_CAST") + (it as PropertyValue).set(true) + } + overrideMode?.propertyValue?.let { + @Suppress("UNCHECKED_CAST") + (it as PropertyValue).set("always_ask") + } + context.config.writeConfig() + } + ) + } + + private fun snapchatMenuReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("purrfectsnap menu") || + (normalized.contains("menu") && normalized.contains("snapchat")) || + normalized.contains("settings in snapchat") + if (!relevant) return null + return AssistantResult( + "To be able to access PurrfectSnap settings in Snapchat directly for faster access, follow this:\n\n" + + "Open Snapchat, go to the chat tab and click on the chat text at the top, a menu will pop up. If it doesn't, ensure that you have given display over other apps permission to PurrfectSnap and set Settings Menu to default in PurrfectSnap." + ) + } + + private fun crashLogsReply(normalized: String): AssistantResult? { + val relevant = listOf("crash log", "crash logs", "logfox", "report crash", "grab logs").any { normalized.contains(it) } + if (!relevant) return null + return AssistantResult( + "To grab crash logs, please follow these steps:\n\n" + + "Non root:\n\n" + + "Download Logfox from GitHub: https://github.com/F0x1d/LogFox/releases\n" + + "Install it, open it, and set it up with Shizuku. You need Shizuku, so make sure it is set up first.\n" + + "Keep Logfox running in the background. You will see a \"running\" notification.\n" + + "Open Snapchat and let it crash. You will receive a notification from Logfox; tap it, copy the crash log, and send it in the Issues topic.\n\n" + + "Root:\n\n" + + "Download Logfox from GitHub: https://github.com/F0x1d/LogFox/releases\n" + + "Open it and grant root permissions.\n" + + "Keep Logfox running in the background. You will see a \"running\" notification.\n" + + "Open Snapchat and let it crash. You will receive a notification from Logfox; tap it, copy the crash log, and send it in the Issues topic." + ) + } + + private fun safetyReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("safe") || normalized.contains("ban") || normalized.contains("banned") || + normalized.contains("main account") || normalized.contains("my account") + if (!relevant) return null + return AssistantResult("It's 100% safe and ban-proof. Only new accounts get banned easily, so it is always recommended to use accounts that are at least 6 months old. PurrfectSnap is open source and completely free, so rest assured, it is completely safe.") + } + + private fun deviceBanReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("device banned") || normalized.contains("ss06") + if (!relevant) return null + return AssistantResult("I have applied the necessary settings to fix it. Force stop and reopen Snapchat and log in.", execute = { + featureCatalog.firstOrNull { it.id == "spoof" }?.container?.globalState = true + featureCatalog.firstOrNull { it.path.contains("Randomized Device Profile", ignoreCase = true) && it.isContainerToggle }?.container?.globalState = true + context.config.writeConfig() + }) + } + + private fun duplicateMessagesReply(normalized: String): AssistantResult? { + val describesProblem = listOf("issue", "problem", "facing", "seeing", "getting", "having", "i have", "i see").any { + normalized.contains(it) + } + val relevant = (normalized.contains("duplicate") || normalized.contains("duplicating") || + normalized.contains("same message") || normalized.contains("repeated message") || + normalized.contains("disappearing message") || normalized.contains("dissapearing message")) && + (normalized.contains("message") || normalized.contains("messages")) && + describesProblem + if (!relevant) return null + return AssistantResult("The Message Translator feature was causing it, so I turned it off.", execute = { + featureCatalog.firstOrNull { it.id == "instant_translation" }?.container?.globalState = false + featureCatalog.firstOrNull { it.id == "enabled" && it.path.contains("Translation", ignoreCase = true) }?.propertyValue?.let { + @Suppress("UNCHECKED_CAST") + (it as PropertyValue).set(false) + } + context.config.writeConfig() + }) + } + + private fun bugReportReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("report bug") || normalized.contains("report bugs") || + normalized.contains("facing an issue") || normalized.contains("how do i report") + if (!relevant) return null + return AssistantResult("First, tell me what issue you are facing here. If I can help, great. If it needs developer attention, please report it in the Telegram group with proper crash logs if required.") + } + + private fun bestFeaturesReply(normalized: String): AssistantResult? { + if (normalized.contains("privacy") && (normalized.contains("best") || normalized.contains("feature") || normalized.contains("recommend"))) { + return AssistantResult("For privacy, use Bypass Screenshot Detection, Hide Typing Notifications, Stealth Mode, and Message Logger controls.") + } + val relevant = normalized.contains("best features") || normalized.contains("recommend features") || normalized.contains("surprise me") + if (!relevant || normalized.contains("tracker rule")) return null + return AssistantResult("Some of the best PurrfectSnap features are Friend Tracker, Media Downloader, Bypass Screenshot Detection, Hide Typing Indicator, Stealth Mode, Snapchat Plus controls, and Auto Save tools.") + } + + private fun downloadSnapsReply(normalized: String): AssistantResult? { + val relevant = (normalized.contains("download") || normalized.contains("save")) && + (normalized.contains("snap") || normalized.contains("snaps") || normalized.contains("media")) + if (!relevant) return null + return AssistantResult("Yes, you can download snaps using PurrfectSnap's downloader features. Enable the download button or auto-download sources in the Downloader settings.") + } + + private fun hideTypingIndicatorReply(normalized: String): AssistantResult? { + val relevant = normalized.contains("typing indicator") || normalized.contains("hide typing") || normalized.contains("typing notification") + if (!relevant) return null + val asksExplanation = listOf("what does", "what is", "what can", "explain", "tell me about", "how does", "meaning", "purpose", "details", "info").any { + normalized.contains(it) + } + if (asksExplanation) return null + val wantsDisable = listOf("disable", "turn off", "deactivate", "switch off").any { normalized.contains(it) } || + Regex("\\bswitch\\b.*\\boff\\b").containsMatchIn(normalized) + val wantsEnable = listOf("enable", "turn on", "activate", "switch on").any { normalized.contains(it) } || + Regex("\\bswitch\\b.*\\bon\\b").containsMatchIn(normalized) + if (wantsEnable || wantsDisable) { + return AssistantResult( + if (wantsDisable) "Disabled Hide Typing Notifications." else "Enabled Hide Typing Notifications.", + execute = { + setBooleanFeatureEnabled( + ids = listOf("hide_typing_notifications"), + phraseHints = listOf("Hide Typing Notifications", "typing indicator", "typing notification"), + enabled = !wantsDisable + ) + } + ) + } + return AssistantResult("Yes, enable Hide Typing Notifications to hide your typing indicator.") + } + + private fun screenshotIndicatorReply(normalized: String): AssistantResult? { + val relevant = (normalized.contains("screenshot") || normalized.contains("screen record") || normalized.contains("record notification")) && + (normalized.contains("indicator") || normalized.contains("without") || normalized.contains("detection") || + normalized.contains("notification") || normalized.contains("from chat") || normalized.contains("hide")) + if (!relevant) return null + val asksExplanation = listOf("what does", "what is", "what can", "explain", "tell me about", "how does", "meaning", "purpose", "details", "info").any { + normalized.contains(it) + } + if (asksExplanation) return null + val wantsDisable = listOf("disable", "turn off", "deactivate", "switch off").any { normalized.contains(it) } || + Regex("\\bswitch\\b.*\\boff\\b").containsMatchIn(normalized) + val wantsEnable = listOf("enable", "turn on", "activate", "switch on").any { normalized.contains(it) } || + Regex("\\bswitch\\b.*\\bon\\b").containsMatchIn(normalized) || + normalized == "yes" || normalized == "yeah" || normalized == "yep" || normalized == "sure" || + normalized == "ok" || normalized == "okay" || normalized == "enable it" || normalized == "do it" + if (!wantsEnable && !wantsDisable && listOf("is there", "is it possible", "can i", "can you", "option").any { normalized.contains(it) }) { + return AssistantResult("Yes, Bypass Screenshot Detection can hide screenshot and screen-record notifications from chat. Do you want me to enable it?") + } + return AssistantResult( + if (wantsDisable) "Disabled Bypass Screenshot Detection." else "Enabled Bypass Screenshot Detection.", + execute = { + setBooleanFeatureEnabled( + ids = listOf("bypass_screenshot_detection"), + phraseHints = listOf("Bypass Screenshot Detection", "screenshot detection", "screen record"), + enabled = !wantsDisable + ) + } + ) + } + + private fun setBooleanFeatureEnabled(ids: List, phraseHints: List, enabled: Boolean) { + val feature = featureCatalog.firstOrNull { feature -> + feature.id in ids || phraseHints.any { hint -> + feature.name.contains(hint, ignoreCase = true) || + feature.path.contains(hint, ignoreCase = true) || + feature.searchPhrases.any { it.contains(hint, ignoreCase = true) } + } + } ?: return + feature.container?.globalState = enabled + feature.propertyValue?.let { + @Suppress("UNCHECKED_CAST") + (it as? PropertyValue)?.set(enabled) + } + context.config.writeConfig() + } + + private fun capabilityReply(normalized: String): AssistantResult? { + val capabilityTerms = listOf("what can you do", "help", "capabilities", "how can you help") + if (capabilityTerms.none { normalized.contains(it) }) return null + return AssistantResult( + "I can explain app features, open sections like logs, settings, features, social, scripts, and friend tracker, change supported settings, launch quick actions, and create basic friend tracker rules from natural language." + ) + } + + private fun unsupportedReply(normalized: String): AssistantResult? { + val phrase = unsupportedPhrases.firstOrNull { normalized.contains(it) } ?: return null + if (featureCatalog.any { feature -> feature.searchPhrases.any { normalize(it) == phrase } }) return null + return AssistantResult("I can only control PurrfectSnap features. \"$phrase\" is not a PurrfectSnap setting or tool, so I won't guess a replacement.") + } + + private fun trackerRuleReply(rawQuery: String, normalized: String): AssistantResult? { + val mentionsTrackerEvent = bestTrackerEventMatch(normalized) != null + val wantsTrackerRule = normalized.contains("tracker rule") || + normalized.contains("friend tracker") || + normalized.contains("tracking rule") || + (normalized.contains("rule") && mentionsTrackerEvent) + val createIntent = listOf("create", "make", "add", "new").any { normalized.contains(it) } + if (!wantsTrackerRule || !createIntent) return null + + val event = bestTrackerEventMatch(normalized) + ?: return AssistantResult("I can create the rule, but I need the trigger event. Try something like \"create a friend tracker rule for typing\" or \"...for screenshot\".") + val actions = bestTrackerActions(normalized).ifEmpty { listOf(TrackerRuleAction.LOG) } + val scopeTargets = resolveScopeTargets(normalized) + val scopeType = if (scopeTargets.isEmpty()) null else TrackerScopeType.WHITELIST + val requestedName = Regex("(named|called)\\s+['\\\"]?([^'\\\"]+)['\\\"]?", RegexOption.IGNORE_CASE) + .find(rawQuery) + ?.groupValues + ?.getOrNull(2) + ?.trim() + ?.takeIf { it.isNotBlank() } + val ruleName = requestedName ?: buildDefaultRuleName(event, scopeTargets) + + if (context.database.getTrackerRuleByName(ruleName) != null) { + return AssistantResult("A friend tracker rule named \"$ruleName\" already exists. Use a different name or edit the existing rule.") + } + + val actionLabels = actions.joinToString(", ") { translateTrackerAction(it) } + val scopeSummary = scopeTargets.takeIf { it.isNotEmpty() }?.joinToString(", ") { it.displayName } ?: "all tracked conversations" + + return AssistantResult( + reply = "Created \"$ruleName\" for ${translateTrackerEvent(event)} with $actionLabels on $scopeSummary.", + execute = { + val ruleId = context.database.newTrackerRule(ruleName, "Purrfect Assistant") + context.database.setTrackerRuleState(ruleId, true) + context.database.addOrUpdateTrackerRuleEvent( + ruleId = ruleId, + eventType = event.key, + params = TrackerRuleActionParams(), + actions = actions + ) + if (scopeType != null) { + context.database.setRuleTrackerScopes(ruleId, scopeType, scopeTargets.map { it.id }) + } + routes.editRule.navigate { + put("rule_id", ruleId.toString()) + } + } + ) + } + + private fun featureMutationReply(rawQuery: String, normalized: String): AssistantResult? { + val mutationFeature = bestDirectFeatureMatch(normalized) + ?: bestFeatureMatch(normalized) + ?: bestLenientFeatureMatch(normalized) + ?: return null + if (!mutationFeature.isProperty && !mutationFeature.isContainerToggle) return null + + val desiredBoolean = parseDesiredBoolean(normalized) + val valueAfterTo = rawQuery.substringAfter(" to ", "").trim().takeIf { rawQuery.contains(" to ", ignoreCase = true) && it.isNotBlank() } + val valueAfterAs = rawQuery.substringAfter(" as ", "").trim().takeIf { rawQuery.contains(" as ", ignoreCase = true) && it.isNotBlank() } + val valueCandidate = valueAfterTo ?: valueAfterAs + + if (mutationFeature.isContainerToggle) { + val desiredState = desiredBoolean ?: return null + return AssistantResult( + reply = "${if (desiredState) "Enabled" else "Disabled"} ${mutationFeature.name}.", + execute = { + mutationFeature.container?.globalState = desiredState + context.config.writeConfig() + } + ) + } + + val propertyKey = mutationFeature.propertyKey ?: return null + val propertyValue = mutationFeature.propertyValue ?: return null + + return when (propertyKey.dataType.type) { + DataProcessors.Type.BOOLEAN -> { + val desiredState = desiredBoolean ?: return null + AssistantResult( + reply = "${if (desiredState) "Enabled" else "Disabled"} ${mutationFeature.name}.", + execute = { + @Suppress("UNCHECKED_CAST") + (propertyValue as PropertyValue).set(desiredState) + context.config.writeConfig() + } + ) + } + + DataProcessors.Type.STRING_UNIQUE_SELECTION -> { + val explicitValue = valueCandidate ?: extractExplicitSetValue(rawQuery) + val option = explicitValue?.let { matchUniqueOption(propertyKey, it) } + ?: desiredBoolean?.let { desired -> + resolveBooleanUniqueOption(propertyKey, desired) + } + ?: matchUniqueOption(propertyKey, normalized) + ?: return null + if (option == UNIQUE_OPTION_AMBIGUOUS) { + return AssistantResult(buildUniqueOptionFollowUp(mutationFeature, propertyKey)) + } + AssistantResult( + reply = "Set ${mutationFeature.name} to ${translateOption(propertyKey, option)}.", + execute = { + @Suppress("UNCHECKED_CAST") + (propertyValue as PropertyValue).set(option) + context.config.writeConfig() + } + ) + } + + DataProcessors.Type.INTEGER -> { + val number = valueCandidate?.toIntOrNull() ?: normalized.filter { it.isDigit() }.toIntOrNull() ?: return null + AssistantResult( + reply = "Set ${mutationFeature.name} to $number.", + execute = { + @Suppress("UNCHECKED_CAST") + (propertyValue as PropertyValue).set(number) + context.config.writeConfig() + } + ) + } + + DataProcessors.Type.FLOAT -> { + val number = valueCandidate?.toFloatOrNull() ?: Regex("(\\d+(?:\\.\\d+)?)").find(normalized)?.value?.toFloatOrNull() ?: return null + AssistantResult( + reply = "Set ${mutationFeature.name} to $number.", + execute = { + @Suppress("UNCHECKED_CAST") + (propertyValue as PropertyValue).set(number) + context.config.writeConfig() + } + ) + } + + DataProcessors.Type.STRING -> { + val text = valueCandidate?.takeIf { it.isNotBlank() } ?: return null + AssistantResult( + reply = "Set ${mutationFeature.name} to \"$text\".", + execute = { + @Suppress("UNCHECKED_CAST") + (propertyValue as PropertyValue).set(text) + context.config.writeConfig() + } + ) + } + + else -> null + } + } + + private fun extractExplicitSetValue(rawQuery: String): String? { + val patterns = listOf( + Regex("\\bset\\b.+?\\bto\\b\\s+(.+)$", RegexOption.IGNORE_CASE), + Regex("\\bmake\\b.+?\\bto\\b\\s+(.+)$", RegexOption.IGNORE_CASE), + Regex("\\bchange\\b.+?\\bto\\b\\s+(.+)$", RegexOption.IGNORE_CASE), + Regex("\\bas\\b\\s+(.+)$", RegexOption.IGNORE_CASE) + ) + return patterns.firstNotNullOfOrNull { pattern -> + pattern.find(rawQuery)?.groupValues?.getOrNull(1)?.trim()?.takeIf { it.isNotBlank() } + } + } + + private fun actionReply(normalized: String): AssistantResult? { + val wantsAction = listOf("run", "launch", "start", "open", "clean", "export").any { normalized.contains(it) } + if (!wantsAction) return null + val action = bestDirectActionMatch(normalized) ?: return null + return AssistantResult(reply = "Launching ${action.name}.", execute = action.execute) + } + + private fun navigationReply(normalized: String): AssistantResult? { + val wantsNavigation = listOf("open", "go to", "take me", "show", "navigate").any { normalized.contains(it) } + if (!wantsNavigation) return null + val route = bestDirectRouteMatch(normalized) ?: return null + return AssistantResult(reply = "Opened ${route.name}.", execute = route.navigate) + } + + private fun featureExplanationReply(normalized: String): AssistantResult? { + val feature = bestDirectFeatureMatch(normalized) ?: return null + val currentValue = if (feature.propertyKey?.dataType?.type == DataProcessors.Type.CONTAINER) { + null + } else { + feature.propertyKey?.let { key -> + feature.propertyValue?.let { value -> describeCurrentValue(key, value) } + } ?: if (feature.isContainerToggle) { + null + } else { + feature.container?.globalState?.let { if (it) "Enabled" else "Disabled" } + } + } + val reply = buildString { + append(feature.name) + append(": ") + append(featureSummary(feature)) + append(" Find it in ") + append(feature.path) + append(".") + currentValue?.takeIf { it.isNotBlank() }?.let { + append(" Current value: ") + append(it) + append(".") + } + } + return AssistantResult(reply) + } + + private fun buildCandidates(rawQuery: String, normalized: String): List { + val wantsSetting = parseDesiredBoolean(normalized) != null || normalized.contains("set ") + val wantsExplanation = listOf("what", "explain", "how", "feature").any { normalized.contains(it) } + val wantsNavigation = listOf("open", "go", "show", "navigate").any { normalized.contains(it) } + val desiredBoolean = parseDesiredBoolean(normalized) + + val featureCandidates = featureCatalog + .map { it to scoreFeature(normalized, it) } + .filter { it.second >= 0.42f } + .sortedByDescending { it.second } + .take(4) + .mapNotNull { (feature, _) -> + if (wantsSetting && !feature.isProperty && !feature.isContainerToggle) return@mapNotNull null + AssistantCandidate( + id = feature.id, + kind = if (feature.isProperty || feature.isContainerToggle) "feature" else "info", + title = feature.name, + summary = "${feature.path}. ${featureSummary(feature)}", + execute = when { + wantsSetting && feature.isContainerToggle && desiredBoolean != null -> ({ + feature.container?.globalState = desiredBoolean + context.config.writeConfig() + }) + wantsSetting && feature.isProperty -> buildFeatureSetter(feature, rawQuery, normalized) + else -> null + } + ) + } + + val routeCandidates = if (wantsNavigation) { + routeCatalog + .map { it to scoreCandidate(normalized, it.searchPhrases, it.searchTokens) } + .filter { it.second >= 0.45f } + .sortedByDescending { it.second } + .take(3) + .map { (route, _) -> + AssistantCandidate( + id = route.id, + kind = "route", + title = route.name, + summary = route.description, + execute = route.navigate + ) + } + } else emptyList() + + val actionCandidates = if (wantsNavigation) { + actionCatalog + .map { it to scoreCandidate(normalized, it.searchPhrases, it.searchTokens) } + .filter { it.second >= 0.5f } + .sortedByDescending { it.second } + .take(2) + .map { (action, _) -> + AssistantCandidate( + id = action.id, + kind = "action", + title = action.name, + summary = action.name, + execute = action.execute + ) + } + } else emptyList() + + val all = when { + wantsNavigation -> routeCandidates + actionCandidates + featureCandidates.take(1) + wantsSetting || wantsExplanation -> featureCandidates + routeCandidates.take(1) + else -> featureCandidates + routeCandidates + actionCandidates + } + return all.distinctBy { it.id }.take(5) + } + + private fun buildFeatureSetter(feature: AssistantFeature, rawQuery: String, normalized: String): (() -> Unit)? { + val propertyKey = feature.propertyKey ?: return null + val propertyValue = feature.propertyValue ?: return null + val desiredBoolean = parseDesiredBoolean(normalized) + val valueAfterTo = rawQuery.substringAfter(" to ", "").trim().takeIf { rawQuery.contains(" to ", ignoreCase = true) && it.isNotBlank() } + val valueAfterAs = rawQuery.substringAfter(" as ", "").trim().takeIf { rawQuery.contains(" as ", ignoreCase = true) && it.isNotBlank() } + val valueCandidate = valueAfterTo ?: valueAfterAs ?: normalized + return when (propertyKey.dataType.type) { + DataProcessors.Type.BOOLEAN -> desiredBoolean?.let { target -> + { + @Suppress("UNCHECKED_CAST") + (propertyValue as PropertyValue).set(target) + context.config.writeConfig() + } + } + DataProcessors.Type.STRING_UNIQUE_SELECTION -> matchUniqueOption(propertyKey, valueCandidate)?.let { matched -> + { + @Suppress("UNCHECKED_CAST") + (propertyValue as PropertyValue).set(matched) + context.config.writeConfig() + } + } + DataProcessors.Type.INTEGER -> valueCandidate.toIntOrNull()?.let { target -> + { + @Suppress("UNCHECKED_CAST") + (propertyValue as PropertyValue).set(target) + context.config.writeConfig() + } + } + DataProcessors.Type.FLOAT -> valueCandidate.toFloatOrNull()?.let { target -> + { + @Suppress("UNCHECKED_CAST") + (propertyValue as PropertyValue).set(target) + context.config.writeConfig() + } + } + DataProcessors.Type.STRING -> { + @Suppress("UNCHECKED_CAST") + { + (propertyValue as PropertyValue).set(valueCandidate) + context.config.writeConfig() + } + } + else -> null + } + } + + private fun buildSelectionPrompt(rawQuery: String, candidates: List): String { + return buildString { + appendLine("Choose the best candidate id for the user's request.") + appendLine("Return only one id or NONE.") + appendLine("User: $rawQuery") + appendLine("Candidates:") + candidates.forEach { candidate -> + appendLine("${candidate.id} | ${candidate.kind} | ${candidate.title} | ${candidate.summary}") + } + } + } + + private fun parseSelectedCandidate(rawResponse: String, candidates: List): AssistantCandidate? { + val normalized = normalize(rawResponse) + if (normalized.contains("none")) return null + return candidates.firstOrNull { candidate -> + normalized.contains(normalize(candidate.id)) || normalized.contains(normalize(candidate.title)) + } + } + + private fun formatCandidateReply(rawQuery: String, normalized: String, candidate: AssistantCandidate): String { + val wantsExplanation = listOf("what", "explain", "how", "feature").any { normalized.contains(it) } + val wantsNavigation = listOf("open", "go", "show", "navigate").any { normalized.contains(it) } + val wantsSetting = parseDesiredBoolean(normalized) != null || normalized.contains("set ") + return when { + candidate.kind == "route" || candidate.kind == "action" || wantsNavigation -> "Opened ${candidate.title}." + candidate.kind == "feature" && wantsSetting -> { + val desired = parseDesiredBoolean(normalized) + desired?.let { if (it) "Enabled ${candidate.title}." else "Disabled ${candidate.title}." } + ?: "Updated ${candidate.title}." + } + wantsExplanation || candidate.kind == "info" || candidate.kind == "feature" -> "${candidate.title}: ${candidate.summary}" + else -> rawQuery + } + } + + private fun strictFallbackReply(normalized: String): AssistantResult { + val feature = bestDirectFeatureMatch(normalized) ?: bestFeatureMatch(normalized) + val route = bestDirectRouteMatch(normalized) ?: bestRouteMatch(normalized) + val suggestions = buildList { + feature?.let { add("feature: ${it.name}") } + route?.let { add("section: ${it.name}") } + }.joinToString(", ") + return AssistantResult( + if (suggestions.isNotBlank()) { + "I couldn't confidently execute that yet. Closest matches: $suggestions. Try asking to enable a setting, open a section, or create a tracker rule with a clear event." + } else { + "I couldn't map that to an app feature yet. Try naming the feature, section, or tracker event more directly." + } + ) + } + + private fun ensureRegistrySynced() { + if (registryPrimed) return + val entries = featureCatalog.map { feature -> + val valueType = feature.propertyKey?.dataType?.type?.name ?: if (feature.isContainerToggle) "BOOLEAN" else "INFO" + val allowedValues = when { + feature.isContainerToggle -> listOf("true", "false") + feature.propertyKey?.dataType?.type == DataProcessors.Type.BOOLEAN -> listOf("true", "false") + else -> feature.propertyValue?.defaultValues?.map { it.toString() }.orEmpty() + } + val allowedActions = when { + feature.isContainerToggle || feature.propertyKey?.dataType?.type == DataProcessors.Type.BOOLEAN -> + listOf("toggle_setting", "search_feature") + feature.isProperty -> listOf("set_option", "search_feature") + else -> listOf("search_feature") + } + me.eternal.purrfectsnap.storage.AssistantRegistryEntry( + id = feature.id, + kind = "feature", + title = feature.name, + category = feature.path.substringBefore(" > ", "Features"), + path = feature.path, + description = featureSummary(feature), + settingKey = feature.propertyKey?.name, + screenRoute = routes.features.routeInfo.id, + allowedActions = allowedActions, + allowedValues = allowedValues, + aliases = feature.searchPhrases, + commonTypos = emptyList(), + examples = listOf( + "What does ${feature.name} do?", + "Open ${feature.name}", + "Enable ${feature.name}" + ), + searchTokens = feature.searchTokens + ) + } + routeCatalog.map { route -> + me.eternal.purrfectsnap.storage.AssistantRegistryEntry( + id = route.id, + kind = "route", + title = route.name, + category = "Navigation", + path = route.name, + description = route.description, + screenRoute = route.id, + allowedActions = listOf("open_screen", "search_feature"), + aliases = route.searchPhrases, + examples = listOf("Open ${route.name}", "Go to ${route.name}"), + searchTokens = route.searchTokens + ) + } + actionCatalog.map { action -> + me.eternal.purrfectsnap.storage.AssistantRegistryEntry( + id = action.id, + kind = "action", + title = action.name, + category = "Action", + path = action.name, + description = action.name, + allowedActions = listOf("open_screen", "search_feature"), + aliases = action.searchPhrases, + examples = listOf("Run ${action.name}", "Launch ${action.name}"), + searchTokens = action.searchTokens + ) + } + context.database.replaceAssistantRegistry(entries) + registryPrimed = true + } + + private fun buildRetrievedDocuments(normalized: String): List { + val query = normalize(normalized) + val registryEntries = context.database.getAssistantRegistryEntries() + val directIds = buildSet { + bestDirectFeatureMatch(query)?.id?.let(::add) + bestDirectRouteMatch(query)?.id?.let(::add) + bestDirectActionMatch(query)?.id?.let(::add) + } + + fun toRegistryDoc(entry: me.eternal.purrfectsnap.storage.AssistantRegistryEntry): String { + return buildString { + append(entry.kind) + append("|id=") + append(entry.id) + append("|title=") + append(escapeForJson(entry.title)) + append("|path=") + append(escapeForJson(entry.path)) + append("|desc=") + append(escapeForJson(entry.description)) + append("|route=") + append(escapeForJson(entry.screenRoute.orEmpty())) + append("|actions=") + append(escapeForJson(entry.allowedActions.joinToString(","))) + append("|values=") + append(escapeForJson(entry.allowedValues.joinToString(","))) + append("|aliases=") + append(escapeForJson(entry.aliases.take(6).joinToString(","))) + } + } + + val forcedDocs = registryEntries + .filter { it.id in directIds } + .take(2) + .map(::toRegistryDoc) + + val scored = registryEntries.map { entry -> + val phrases = buildList { + add(entry.title) + add(entry.description) + add(entry.path) + addAll(entry.aliases) + addAll(entry.commonTypos) + addAll(entry.examples) + }.filter { it.isNotBlank() } + val exactBoost = if (phrases.any { normalize(it) == query }) 3f else 0f + val fuzzyScore = scoreCandidate(query, phrases, entry.searchTokens + phrases.flatMap { tokenize(it) }) + val semanticScore = scoreTokens(query, entry.searchTokens + tokenize(entry.description) + entry.examples.flatMap { tokenize(it) }) + entry to (exactBoost + max(fuzzyScore, semanticScore)) + } + .filter { it.second >= 0.42f } + .sortedByDescending { it.second } + .map { (entry, _) -> entry } + .filterNot { it.id in directIds } + .take(maxOf(0, 2 - forcedDocs.size)) + .map(::toRegistryDoc) + + val trackerDoc = buildString { + append("tracker|id=friend_tracker_rule|title=Friend Tracker Rule|path=Friend Tracker|desc=Create friend tracker rules.") + append("|actions=create_tracker_rule") + append("|values=") + append( + escapeForJson( + (TrackerEventType.entries.map { it.key } + TrackerRuleAction.entries.map { it.key }) + .joinToString(",") + ) + ) + append("|aliases=friend tracker,tracker rule") + } + + return forcedDocs + scored + trackerDoc + } + + private fun escapeForJson(value: String): String { + return value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", " ") + } + + private fun featureAliasesForKey(keyName: String): List { + return when (keyName) { + "friend_tracker" -> listOf("friend tracker", "tracker", "tracker feature", "friend tracking") + "snapchat_plus" -> listOf("snapchat plus", "snap plus", "snapchat premium", "plus", "snapchat pluys", "snapchta plus", "snap plus feature") + "merge_overlays" -> listOf("merge overlay", "merge overlays", "overlay merge", "merge overlay feature", "merge overlays feature") + "hidden_snapchat_plus_features" -> listOf("hidden snap features", "hidden snapchat features", "hidden snap plus features", "hidden snapchat plus features") + "manager_theme" -> listOf("manager theme", "theme", "aphelion theme", "legacy theme") + "haptic_feedback" -> listOf("haptic", "haptic feedback", "vibration", "vibrate") + "record_messaging_events" -> listOf("record tracker events", "tracker event logging") + "allow_running_in_background" -> listOf("background tracking", "run in background") + "custom_video_codec" -> listOf("video codec", "custom video codec") + "custom_audio_codec" -> listOf("audio codec") + "auto_purge" -> listOf("auto purge", "purge") + "bypass_screenshot_detection" -> listOf( + "bypass screenshot detection", + "hide screenshot notification", + "hide screenshot indicator", + "hide screen record notification", + "screenshot without notification", + "record without notification" + ) + "hide_typing_notifications" -> listOf("hide typing indicator", "hide typing notification", "typing indicator") + "download_button" -> listOf("download snaps", "save snaps", "download button", "media downloader") + "instant_translation" -> listOf("message translator", "message translation", "translator causing duplicate messages") + else -> emptyList() + } + } + + private fun featureSummary(feature: AssistantFeature): String { + feature.description.takeIf { it.isNotBlank() }?.let { return it } + return when (feature.id) { + "friend_tracker" -> "Tracks Snapchat presence and messaging events so you can create rules for typing, speaking, screenshots, opens, and other activity." + "snapchat_plus" -> "Controls the Snapchat Plus subscription state PurrfectSnap reports to Snapchat." + "merge_overlays" -> "Merges downloaded media overlays into the saved media output." + "hidden_snapchat_plus_features" -> "Unlocks hidden Snapchat Plus feature switches exposed by PurrfectSnap." + "haptic_feedback" -> "Controls vibration feedback for manager interactions." + "manager_theme" -> "Changes the manager UI theme." + "record_messaging_events" -> "Stores messaging events for friend tracker rules." + "allow_running_in_background" -> "Lets the friend tracker keep running while the app is not foregrounded." + "bypass_screenshot_detection" -> "Hides screenshot and screen-record detection notifications from Snapchat chats." + "hide_typing_notifications" -> "Hides your typing indicator from chats." + "download_button" -> "Adds downloader controls for saving snaps and media." + else -> "This is part of the app configuration." + } + } + + private fun buildFeatureCatalog(): List { + val features = mutableListOf() + fun walk(container: ConfigContainer, pathSegments: List) { + container.properties.forEach { (key, value) -> + val isHidden = key.params.flags.contains(ConfigFlag.HIDDEN) + val translatedName = translated(key.propertyName(), humanize(key.name)) + val translatedDescription = translated(key.propertyDescription(), "") + val path = (pathSegments + translatedName).joinToString(" > ") + val phrases = buildList { + add(translatedName) + add(translatedDescription) + add(path) + add(key.name) + addAll(featureAliasesForKey(key.name)) + key.params.disabledKey?.let { add(it) } + value.defaultValues?.forEach { add(it.toString()) } + }.filter { it.isNotBlank() }.distinct() + val tokens = phrases.flatMap { tokenize(it) }.distinct() + + if (key.dataType.type == DataProcessors.Type.CONTAINER) { + val child = value.get() as? ConfigContainer ?: return@forEach + if (!isHidden) { + features += AssistantFeature( + id = key.name, + name = translatedName, + description = translatedDescription, + path = path, + searchPhrases = phrases, + searchTokens = tokens, + container = child, + propertyKey = key, + propertyValue = value + ) + } + walk(child, if (isHidden) pathSegments else pathSegments + translatedName) + } else { + features += AssistantFeature( + id = key.name, + name = translatedName, + description = translatedDescription, + path = path, + searchPhrases = phrases, + searchTokens = tokens, + propertyKey = key, + propertyValue = value + ) + } + } + } + walk(context.config.root, emptyList()) + return features + } + + private fun buildRouteCatalog(): List { + fun route( + routeId: String, + name: String, + description: String, + vararg aliases: String, + navigate: () -> Unit + ) = AssistantRoute( + id = routeId, + name = name, + description = description, + searchPhrases = buildList { + add(name) + add(description) + aliases.forEach { add(it) } + }.distinct(), + searchTokens = buildList { + add(name) + add(description) + aliases.forEach { add(it) } + }.flatMap { tokenize(it) }.distinct(), + navigate = navigate + ) + + return listOf( + route(routes.home.routeInfo.id, "Home", "Main dashboard", "home", "dashboard") { routes.home.navigateReset() }, + route(routes.homeLogs.routeInfo.id, "Logs", "App log overview", "logs", "home logs", "logger") { routes.homeLogs.navigateReset() }, + route(routes.about.routeInfo.id, "About", "App overview and about", "about", "info") { routes.about.navigateReset() }, + route(routes.settings.routeInfo.id, "Settings", "Home settings", "settings", "preferences") { routes.settings.navigateReset() }, + route(routes.features.routeInfo.id, "Features", "Feature configuration", "features", "feature settings") { routes.features.navigateReset() }, + route(routes.social.routeInfo.id, "Social", "Social tools and insights", "social", "friends", "groups") { routes.social.navigateReset() }, + route(routes.scripting.routeInfo.id, "Scripts", "Scripting tools", "scripts", "scripting") { routes.scripting.navigateReset() }, + route(routes.friendTracker.routeInfo.id, "Friend Tracker", "Friend tracker management", "friend tracker", "tracker") { routes.friendTracker.navigateReset() }, + route(routes.fileImports.routeInfo.id, "File Imports", "Imported files", "file imports", "imports") { routes.fileImports.navigateReset() }, + route(routes.loggerHistory.routeInfo.id, "Logger History", "Historical logger entries", "logger history", "history") { routes.loggerHistory.navigateReset() } + ) + } + + private fun buildActionCatalog(): List { + val quickActions = EnumQuickActions.entries.map { quick -> + val phrases = listOf(quick.key, humanize(quick.key)) + AssistantAction( + id = quick.key, + name = humanize(quick.key), + searchPhrases = phrases, + searchTokens = phrases.flatMap { tokenize(it) }.distinct(), + execute = { quick.action(routes) } + ) + } + val actions = EnumAction.entries.map { action -> + val phrases = listOf(action.key, humanize(action.key)) + AssistantAction( + id = action.key, + name = humanize(action.key), + searchPhrases = phrases, + searchTokens = phrases.flatMap { tokenize(it) }.distinct(), + execute = { context.launchActionIntent(action) } + ) + } + return quickActions + actions + } + + private fun bestDirectFeatureMatch(normalized: String): AssistantFeature? = + clearWinner(directFeatureCandidates(normalized)) + + private fun bestDirectRouteMatch(normalized: String): AssistantRoute? = + clearWinner(directRouteCandidates(normalized)) + + private fun bestDirectActionMatch(normalized: String): AssistantAction? = + clearWinner(directActionCandidates(normalized)) + + private fun directFeatureCandidates(normalized: String): List> { + return featureCatalog.mapNotNull { feature -> + val score = directPhraseScore(normalized, feature.searchPhrases) + if (score <= 0f) null else feature to score + }.sortedByDescending { it.second } + } + + private fun directRouteCandidates(normalized: String): List> { + return routeCatalog.mapNotNull { route -> + val score = directPhraseScore(normalized, route.searchPhrases) + if (score <= 0f) null else route to score + }.sortedByDescending { it.second } + } + + private fun directActionCandidates(normalized: String): List> { + return actionCatalog.mapNotNull { action -> + val score = directPhraseScore(normalized, action.searchPhrases) + if (score <= 0f) null else action to score + }.sortedByDescending { it.second } + } + + private fun clearWinner(candidates: List>): T? { + val top = candidates.firstOrNull() ?: return null + val runnerUp = candidates.getOrNull(1) + if (top.second < 8f) return null + if (runnerUp != null && top.second < runnerUp.second + 2f) return null + return top.first + } + + private fun bestFeatureMatch(normalized: String): AssistantFeature? { + return featureCatalog.maxByOrNull { feature -> scoreFeature(normalized, feature) } + ?.takeIf { scoreFeature(normalized, it) >= 0.72f } + } + + private fun bestLenientFeatureMatch(normalized: String): AssistantFeature? { + val candidates = featureCatalog + .map { feature -> feature to scoreFeature(normalized, feature) } + .filter { it.second >= 0.32f } + .sortedByDescending { it.second } + val top = candidates.firstOrNull() ?: return null + val second = candidates.getOrNull(1) + if (second != null && top.second < second.second + 0.08f) return null + return top.first + } + + private fun bestRouteMatch(normalized: String): AssistantRoute? { + return routeCatalog.maxByOrNull { route -> scoreCandidate(normalized, route.searchPhrases, route.searchTokens) } + ?.takeIf { scoreCandidate(normalized, it.searchPhrases, it.searchTokens) >= 0.74f } + } + + private fun bestActionMatch(normalized: String): AssistantAction? { + return actionCatalog.maxByOrNull { action -> scoreCandidate(normalized, action.searchPhrases, action.searchTokens) } + ?.takeIf { scoreCandidate(normalized, it.searchPhrases, it.searchTokens) >= 0.76f } + } + + private fun directPhraseScore(normalized: String, phrases: List): Float { + val query = normalize(normalized) + if (query.isBlank()) return 0f + return phrases.maxOfOrNull { phrase -> + val normalizedPhrase = normalize(phrase) + when { + normalizedPhrase.isBlank() -> 0f + query == normalizedPhrase -> 10f + normalizedPhrase.length + query.contains(normalizedPhrase) -> 5f + normalizedPhrase.length + meaningfulTokens(query).containsAll(meaningfulTokens(normalizedPhrase)) && meaningfulTokens(normalizedPhrase).isNotEmpty() -> 3f + normalizedPhrase.length + else -> 0f + } + } ?: 0f + } + + private fun bestTrackerEventMatch(normalized: String): TrackerEventType? { + return TrackerEventType.entries.maxByOrNull { event -> + maxOf( + directPhraseScore(normalized, listOf(event.key, translateTrackerEvent(event)) + trackerEventAliases(event)), + scoreTokens( + normalized, + tokenize(event.key) + + tokenize(translateTrackerEvent(event)) + + trackerEventAliases(event).flatMap { tokenize(it) } + ) + ) + }?.takeIf { + maxOf( + directPhraseScore(normalized, listOf(it.key, translateTrackerEvent(it)) + trackerEventAliases(it)), + scoreTokens( + normalized, + tokenize(it.key) + + tokenize(translateTrackerEvent(it)) + + trackerEventAliases(it).flatMap { tokenize(it) } + ) + ) >= 0.45f + } + } + + private fun bestTrackerActions(normalized: String): List { + return TrackerRuleAction.entries.filter { action -> + scoreTokens(normalized, tokenize(action.key) + tokenize(translateTrackerAction(action))) >= 0.58f + } + } + + private fun trackerEventAliases(event: TrackerEventType): List { + return when (event.key) { + "started_typing" -> listOf("typing", "started typing", "is typing") + "stopped_typing" -> listOf("stopped typing", "typing stopped") + "started_speaking" -> listOf("speaking", "started speaking", "someone speaks", "they speak") + "stopped_speaking" -> listOf("stopped speaking", "speaking stopped") + "message_read" -> listOf("opened chat", "read chat", "chat opened", "read messages", "showed chat") + "snap_opened" -> listOf("opened snap", "viewed snap", "snap opened", "showed snap") + "snap_screenshot" -> listOf("screenshot", "screenshots", "took screenshot", "screen shot") + "snap_screen_record" -> listOf("screen record", "screen recording", "recorded screen") + "message_saved" -> listOf("saved message", "saved in chat") + "message_unsaved" -> listOf("unsaved message", "removed save") + else -> emptyList() + } + } + + private fun resolveScopeTargets(normalized: String): List { + if (normalized.contains("for all")) return emptyList() + val targets = mutableListOf() + context.database.getFriends().forEach { friend -> + val label = friend.displayName?.takeIf { it.isNotBlank() } ?: friend.mutableUsername + val tokens = tokenize(label) + tokenize(friend.mutableUsername) + if (scoreTokens(normalized, tokens) >= 0.78f) { + targets += ScopeTarget(friend.userId, label) + } + } + context.database.getGroups().forEach { group -> + if (scoreTokens(normalized, tokenize(group.name)) >= 0.8f) { + targets += ScopeTarget(group.conversationId, group.name) + } + } + return targets.distinctBy { it.id }.take(3) + } + + private fun translateTrackerEvent(event: TrackerEventType): String { + return translated("tracker_events.${event.key}", humanize(event.key)) + } + + private fun translateTrackerAction(action: TrackerRuleAction): String { + return translated("tracker_actions.${action.key}", humanize(action.key)) + } + + private fun translated(path: String, fallback: String): String { + return runCatching { context.translation[path] }.getOrNull() + ?.takeIf { it.isNotBlank() && it != path } + ?: fallback + } + + private fun buildDefaultRuleName(event: TrackerEventType, targets: List): String { + val base = translateTrackerEvent(event) + val targetSuffix = targets.firstOrNull()?.displayName?.let { " - $it" }.orEmpty() + return "$base Rule$targetSuffix" + } + + private fun parseDesiredBoolean(normalized: String): Boolean? { + return when { + listOf("turn off", "disable", "disabled", "deactivate", "hide", "switch off").any { normalized.contains(it) } -> false + Regex("\\b(turn|switch)\\b.*\\boff\\b").containsMatchIn(normalized) -> false + listOf("turn on", "enable", "enabled", "activate", "show", "switch on").any { normalized.contains(it) } -> true + Regex("\\b(turn|switch)\\b.*\\bon\\b").containsMatchIn(normalized) -> true + else -> null + } + } + + private fun matchUniqueOption(propertyKey: PropertyKey<*>, candidate: String): String? { + val values = uniqueOptionValues(propertyKey) + val normalizedCandidate = normalize(candidate) + val compactCandidate = compactNormalize(candidate) + return values.maxByOrNull { option -> + val terms = uniqueOptionTerms(propertyKey, option) + val tokenScore = scoreTokens(normalizedCandidate, terms.flatMap { tokenize(it) }) + val compactScore = if (terms.any { compactNormalize(it) == compactCandidate }) 1f else 0f + max(tokenScore, compactScore) + }?.takeIf { option -> + val terms = uniqueOptionTerms(propertyKey, option) + val tokenScore = scoreTokens(normalizedCandidate, terms.flatMap { tokenize(it) }) + val compactScore = if (terms.any { compactNormalize(it) == compactCandidate }) 1f else 0f + max(tokenScore, compactScore) >= 0.52f + } + } + + private fun uniqueOptionValues(propertyKey: PropertyKey<*>): List { + return buildList { + propertyKey.params.disabledKey?.let { add(it) } + featureCatalog.firstOrNull { it.propertyKey == propertyKey } + ?.propertyValue + ?.defaultValues + ?.forEach { add(it.toString()) } + }.distinct() + } + + private fun uniqueOptionTerms(propertyKey: PropertyKey<*>, option: String): List { + return buildList { + add(option) + add(translateOption(propertyKey, option)) + add(option.replace('_', ' ')) + add(option.replace("-", " ")) + if (option == "ad_free") { + add("adfree") + add("ad free") + add("without ads") + add("no ads") + } + if (option == "not_subscribed") { + add("disabled") + add("off") + add("not subscribed") + add("no plus") + } + }.filter { it.isNotBlank() }.distinct() + } + + private fun resolveBooleanUniqueOption(propertyKey: PropertyKey<*>, enabled: Boolean): String? { + val values = uniqueOptionValues(propertyKey) + if (!enabled) { + return values.firstOrNull { isDisabledUniqueOption(propertyKey, it) } + ?: propertyKey.params.disabledKey + ?: "null" + } + val enabledOptions = values.filterNot { isDisabledUniqueOption(propertyKey, it) } + return when (enabledOptions.size) { + 0 -> null + 1 -> enabledOptions.first() + else -> UNIQUE_OPTION_AMBIGUOUS + } + } + + private fun isDisabledUniqueOption(propertyKey: PropertyKey<*>, option: String): Boolean { + val normalized = normalize(option) + return option == propertyKey.params.disabledKey || + option == "null" || + normalized in setOf("disabled", "disable", "off", "none", "not subscribed", "not subscribed") + } + + private fun buildUniqueOptionFollowUp(feature: AssistantFeature, propertyKey: PropertyKey<*>): String { + val options = uniqueOptionValues(propertyKey) + .filterNot { isDisabledUniqueOption(propertyKey, it) } + .joinToString(" or ") { translateOption(propertyKey, it) } + return "Which ${feature.name} option: $options?" + } + + private fun translateOption(propertyKey: PropertyKey<*>, option: String): String { + return runCatching { propertyKey.propertyOption(context.translation, option) }.getOrElse { humanize(option) } + } + + private fun describeCurrentValue(propertyKey: PropertyKey<*>, propertyValue: PropertyValue<*>): String { + return when (propertyKey.dataType.type) { + DataProcessors.Type.BOOLEAN -> { + val current = propertyValue.getNullable() as? Boolean ?: false + if (current) "Enabled" else "Disabled" + } + + DataProcessors.Type.CONTAINER -> "Section" + + DataProcessors.Type.STRING_UNIQUE_SELECTION -> { + val current = propertyValue.getNullable()?.toString() ?: propertyKey.params.disabledKey ?: "disabled" + translateOption(propertyKey, current) + } + + DataProcessors.Type.STRING_MULTIPLE_SELECTION -> { + val current = propertyValue.getNullable() as? List<*> + current?.joinToString(", ") ?: "No items selected" + } + + else -> propertyValue.getNullable()?.toString() ?: "Not set" + } + } + + private fun normalize(value: String): String { + return value.lowercase() + .replace(Regex("[^a-z0-9\\s]"), " ") + .replace(Regex("\\s+"), " ") + .trim() + } + + private fun compactNormalize(value: String): String { + return normalize(value).replace(" ", "") + } + + private fun tokenize(value: String): List { + return normalize(value).split(' ').filter { it.isNotBlank() } + } + + private fun meaningfulTokens(value: String): List { + return tokenize(value).filter { it !in stopWords && it.length > 1 } + } + + private fun scoreFeature(query: String, feature: AssistantFeature): Float { + return scoreCandidate(query, feature.searchPhrases, feature.searchTokens) + } + + private fun scoreCandidate(query: String, phrases: List, tokens: List): Float { + val normalizedQuery = normalize(query) + if (normalizedQuery.isBlank()) return 0f + if (phrases.any { normalize(it) == normalizedQuery }) return 1.2f + if (phrases.any { phrase -> + val normalizedPhrase = normalize(phrase) + normalizedPhrase.isNotBlank() && normalizedQuery.contains(normalizedPhrase) + }) { + return 1.05f + } + + val queryTokens = meaningfulTokens(normalizedQuery) + if (queryTokens.isEmpty()) return 0f + val overlap = queryTokens.count { queryToken -> + tokens.any { token -> token == queryToken || token.contains(queryToken) || queryToken.contains(token) } + } + val requiredOverlap = when { + queryTokens.size >= 4 -> 2 + else -> 1 + } + if (overlap < requiredOverlap) return 0f + + val coverage = overlap.toFloat() / queryTokens.size.toFloat() + val fuzzy = tokens.maxOfOrNull { similarity(normalizedQuery, it) } ?: 0f + return max(coverage, fuzzy) + } + + private fun scoreTokens(query: String, tokens: List): Float { + val normalizedQuery = normalize(query) + if (normalizedQuery.isBlank()) return 0f + val fullText = tokens.joinToString(" ") + if (fullText.contains(normalizedQuery)) return 1f + val queryTokens = tokenize(normalizedQuery) + val coverage = if (queryTokens.isEmpty()) 0f else { + queryTokens.count { queryToken -> + tokens.any { token -> token.contains(queryToken) || queryToken.contains(token) } + }.toFloat() / queryTokens.size.toFloat() + } + val fuzzy = tokens.maxOfOrNull { similarity(normalizedQuery, it) } ?: 0f + return max(coverage, fuzzy) + } + + private fun similarity(a: String, b: String): Float { + if (a == b) return 1f + if (a.isBlank() || b.isBlank()) return 0f + val distance = levenshtein(a, b) + val maxLength = max(a.length, b.length) + return 1f - distance.toFloat() / maxLength.toFloat() + } + + private fun levenshtein(a: String, b: String): Int { + if (a == b) return 0 + if (a.isEmpty()) return b.length + if (b.isEmpty()) return a.length + val previous = IntArray(b.length + 1) { it } + val current = IntArray(b.length + 1) + for (i in a.indices) { + current[0] = i + 1 + for (j in b.indices) { + val cost = if (a[i] == b[j]) 0 else 1 + current[j + 1] = min(min(current[j] + 1, previous[j + 1] + 1), previous[j] + cost) + } + for (j in previous.indices) { + previous[j] = current[j] + } + } + return previous[b.length] + } + + private fun humanize(value: String): String { + return value.replace('_', ' ') + .split(' ') + .filter { it.isNotBlank() } + .joinToString(" ") { token -> token.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } } + } +} + +@Composable +fun ManagerAssistantEntry( + context: RemoteSideContext, + routes: Routes, + style: ManagerAssistantTriggerStyle, + shrinkFactor: Float = 1f, + modifier: Modifier = Modifier, + initialUserMessage: String? = null, + showImprovementLogging: Boolean = true +) { + var isOpen by rememberSaveable { mutableStateOf(false) } + ManagerAssistantTrigger(style = style, shrinkFactor = shrinkFactor, modifier = modifier) { + isOpen = true + } + if (isOpen) { + ManagerAssistantDialog( + context = context, + routes = routes, + initialUserMessage = initialUserMessage, + showImprovementLogging = showImprovementLogging, + onDismiss = { isOpen = false } + ) + } +} + +@Composable +private fun ManagerAssistantTrigger( + style: ManagerAssistantTriggerStyle, + shrinkFactor: Float, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + val border = when (style) { + ManagerAssistantTriggerStyle.DEFAULT -> BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)) + ManagerAssistantTriggerStyle.APHELION -> BorderStroke( + 1.dp, + Brush.linearGradient( + listOf( + PurrfectPalette.glowPrimary.copy(alpha = 0.55f), + PurrfectPalette.glowSecondary.copy(alpha = 0.35f) + ) + ) + ) + } + Surface( + modifier = modifier.height(36.dp).defaultMinSize(minWidth = 36.dp), + shape = RoundedCornerShape(40.dp), + color = Color.White.copy(alpha = 0.06f), + border = border + ) { + Row( + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick).padding(vertical = 6.dp, horizontal = (10 * shrinkFactor.coerceIn(0.55f, 1f)).dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon(Icons.Default.SmartToy, contentDescription = "Open assistant", tint = Color.White, modifier = Modifier.size(20.dp)) + val labelAlpha = if (style == ManagerAssistantTriggerStyle.APHELION) (shrinkFactor - 0.1f).coerceIn(0f, 1f) else 1f + if (labelAlpha > 0.02f) { + Spacer(modifier = Modifier.width((8 * shrinkFactor).dp)) + Text( + text = "AI", + color = Color.White.copy(alpha = labelAlpha), + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} + +@Composable +fun ManagerAssistantDialog( + context: RemoteSideContext, + routes: Routes, + initialUserMessage: String? = null, + showImprovementLogging: Boolean = true, + onDismiss: () -> Unit +) { + val scope = rememberCoroutineScope() + val engine = remember(context, routes) { ManagerAssistantEngine(context, routes) } + val messages = remember { + mutableStateListOf( + AssistantMessage( + AssistantRole.ASSISTANT, + "Ask me to explain a feature, open a section, change a setting, or create a friend tracker rule." + ) + ) + } + val suggestions = remember { + mutableStateListOf( + "Open logs", + "What does Friend Tracker do?", + "Enable haptic feedback", + "Create a friend tracker rule for typing" + ) + } + val listState = rememberLazyListState() + var input by rememberSaveable { mutableStateOf("") } + var isWorking by remember { mutableStateOf(false) } + var pendingAssistantFollowUp by rememberSaveable { mutableStateOf(null) } + var trainingLogUri by rememberSaveable { + mutableStateOf(context.sharedPreferences.getString(TRAINING_LOG_URI_PREF, "").orEmpty()) + } + fun appendTrainingLog(userText: String, assistantText: String) { + val uri = trainingLogUri.takeIf { it.isNotBlank() } ?: return + runCatching { + context.androidContext.contentResolver.openOutputStream(Uri.parse(uri), "wa")?.bufferedWriter()?.use { writer -> + writer.appendLine("USER: $userText") + writer.appendLine("ASSISTANT: $assistantText") + writer.appendLine("---") + } + }.onFailure { + context.log.error("Failed to append assistant training log", it) + } + } + + fun chooseTrainingLogFile() { + routes.activityLauncher.saveFile("purrfectsnap-ai-training-log.txt", "text/plain") { uri -> + if (uri.isNotBlank()) { + trainingLogUri = uri + context.sharedPreferences.edit().putString(TRAINING_LOG_URI_PREF, uri).apply() + context.shortToast("Assistant training log enabled") + } + } + } + + fun submitQuery(query: String) { + val trimmed = query.trim() + if (trimmed.isBlank() || isWorking) return + messages += AssistantMessage(AssistantRole.USER, trimmed) + input = "" + isWorking = true + scope.launch { + val normalizedReply = trimmed.lowercase().trim() + val resolvedQuery = when { + pendingAssistantFollowUp == "fake_snap_guide" && + normalizedReply in setOf("yes", "yeah", "yep", "sure", "ok", "okay", "guide me", "do it") -> + "how to remove the media upload tag" + pendingAssistantFollowUp == "screenshot_detection_enable" && + normalizedReply in setOf("yes", "yeah", "yep", "sure", "ok", "okay", "enable it", "do it") -> + "enable bypass screenshot detection" + else -> trimmed + } + pendingAssistantFollowUp = null + val result = runCatching { + withContext(Dispatchers.Default) { engine.handle(resolvedQuery) } + }.getOrElse { + context.log.error("Assistant query failed", it) + AssistantResult("Assistant failed to process that request. Please try a clearer feature, setting, or section name.") + } + result.execute?.invoke() + messages += AssistantMessage(AssistantRole.ASSISTANT, result.reply) + appendTrainingLog(trimmed, result.reply) + pendingAssistantFollowUp = when { + result.reply.contains("Do you want me to guide you?", ignoreCase = true) -> "fake_snap_guide" + result.reply.contains("Do you want me to enable it?", ignoreCase = true) -> "screenshot_detection_enable" + else -> null + } + isWorking = false + } + } + + LaunchedEffect(messages.size, isWorking) { + if (messages.isNotEmpty()) { + listState.animateScrollToItem(messages.lastIndex) + } + } + + LaunchedEffect(initialUserMessage) { + initialUserMessage?.takeIf { it.isNotBlank() }?.let { + if (messages.none { message -> message.role == AssistantRole.USER }) { + submitQuery(it) + } + } + } + + Dialog(onDismissRequest = onDismiss) { + Surface( + modifier = Modifier.fillMaxWidth().padding(12.dp), + shape = RoundedCornerShape(26.dp), + color = Color.Transparent, + tonalElevation = 0.dp, + shadowElevation = 18.dp, + border = BorderStroke( + 1.dp, + Brush.linearGradient( + listOf( + PurrfectPalette.glowPrimary.copy(alpha = 0.55f), + PurrfectPalette.glowSecondary.copy(alpha = 0.42f) + ) + ) + ) + ) { + Box( + modifier = Modifier + .background(PurrfectPalette.cardOverlay, RoundedCornerShape(26.dp)) + .padding(18.dp) + ) { + Column( + modifier = Modifier.fillMaxWidth().heightIn(min = 420.dp, max = 680.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Surface( + modifier = Modifier.size(42.dp), + shape = RoundedCornerShape(14.dp), + color = PurrfectPalette.glowPrimary.copy(alpha = 0.18f) + ) { + Box(contentAlignment = Alignment.Center) { + Icon(Icons.Default.SmartToy, contentDescription = null, tint = Color.White) + } + } + Column { + Text( + text = "PurrfectSnap AI", + style = MaterialTheme.typography.titleMedium, + color = Color.White, + fontWeight = FontWeight.Bold + ) + } + } + IconButton(onClick = onDismiss) { + Text("x", color = Color.White, fontSize = 20.sp) + } + } + + if (showImprovementLogging) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AssistChip( + onClick = { chooseTrainingLogFile() }, + label = { + Text( + if (trainingLogUri.isBlank()) "Contribute assistant improvement data?" + else "Assistant improvement logging enabled", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + colors = AssistChipDefaults.assistChipColors( + containerColor = Color.White.copy(alpha = 0.08f), + labelColor = Color.White, + leadingIconContentColor = Color.White + ), + leadingIcon = { Icon(Icons.Default.SmartToy, contentDescription = null) } + ) + } + } + + LazyColumn( + modifier = Modifier.fillMaxWidth().weight(1f), + state = listState, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + items(messages) { message -> + AssistantBubble(message = message) + } + if (isWorking) { + item { + Surface(shape = RoundedCornerShape(18.dp), color = Color.White.copy(alpha = 0.06f)) { + Row( + modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = Color.White) + Text("Working on that...", color = Color.White) + } + } + } + } + } + + if (messages.size <= 1) { + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + suggestions.forEach { suggestion -> + AssistChip( + onClick = { submitQuery(suggestion) }, + label = { Text(suggestion, maxLines = 1, overflow = TextOverflow.Ellipsis) }, + colors = AssistChipDefaults.assistChipColors( + containerColor = Color.White.copy(alpha = 0.08f), + labelColor = Color.White, + leadingIconContentColor = Color.White + ), + leadingIcon = { Icon(Icons.Default.SmartToy, contentDescription = null) } + ) + } + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.Bottom + ) { + OutlinedTextField( + value = input, + onValueChange = { input = it }, + modifier = Modifier.weight(1f), + placeholder = { Text("Ask or command the app...", color = PurrfectPalette.textSecondary) }, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.White.copy(alpha = 0.05f), + unfocusedContainerColor = Color.White.copy(alpha = 0.04f), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedTextColor = Color.White, + unfocusedTextColor = Color.White, + cursorColor = Color.White + ), + shape = RoundedCornerShape(18.dp), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), + keyboardActions = KeyboardActions(onSend = { submitQuery(input) }), + maxLines = 4 + ) + Button( + onClick = { submitQuery(input) }, + enabled = input.isNotBlank() && !isWorking, + colors = ButtonDefaults.buttonColors( + containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.32f), + contentColor = Color.White + ), + shape = RoundedCornerShape(18.dp), + modifier = Modifier.height(56.dp).wrapContentWidth() + ) { + Icon(Icons.Default.Send, contentDescription = "Send") + } + } + } + } + } + } +} + +@Composable +private fun AssistantBubble(message: AssistantMessage) { + val isUser = message.role == AssistantRole.USER + val shape = RoundedCornerShape( + topStart = 18.dp, + topEnd = 18.dp, + bottomStart = if (isUser) 18.dp else 6.dp, + bottomEnd = if (isUser) 6.dp else 18.dp + ) + val background = if (isUser) { + Brush.linearGradient( + listOf( + PurrfectPalette.glowPrimary.copy(alpha = 0.34f), + PurrfectPalette.glowSecondary.copy(alpha = 0.28f) + ) + ) + } else { + Brush.linearGradient( + listOf( + Color.White.copy(alpha = 0.08f), + Color.White.copy(alpha = 0.05f) + ) + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = if (isUser) Arrangement.End else Arrangement.Start + ) { + Surface( + modifier = Modifier.widthIn(max = 520.dp), + shape = shape, + color = Color.Transparent, + border = BorderStroke(1.dp, Brush.linearGradient(listOf(Color.White.copy(alpha = 0.12f), Color.White.copy(alpha = 0.08f)))) + ) { + Box( + modifier = Modifier.background(background, shape).padding(horizontal = 14.dp, vertical = 12.dp) + ) { + Text(text = message.text, color = Color.White, style = MaterialTheme.typography.bodyMedium) + } + } + } +} diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/MainActivity.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/MainActivity.kt index 1ebd85ef..f677a91f 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/MainActivity.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/MainActivity.kt @@ -133,16 +133,16 @@ class MainActivity : ComponentActivity() { if (shouldShowAbiWarning) { AestheticDialog( onDismissRequest = {}, - title = managerContext.translation["wrong_apk_title"], + title = managerContext.translation["setup.activity.wrong_apk_title"], text = "", icon = Icons.Filled.Warning, - confirmButtonText = managerContext.translation["common.close"], + confirmButtonText = managerContext.translation["setup.activity.close_button"], onConfirm = { (context as? Activity)?.finishAffinity() }, showCloseButton = false, opaque = true, customContent = { Text( - text = managerContext.translation["wrong_apk_message"], + text = managerContext.translation["setup.activity.wrong_apk_message"], color = PurrfectPalette.textSecondary, lineHeight = 18.sp ) diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/components/FloatingTopBar.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/components/FloatingTopBar.kt index f12962b8..5ded43bd 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/components/FloatingTopBar.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/components/FloatingTopBar.kt @@ -269,7 +269,7 @@ fun FloatingTopBar( if (onBack != null) { translationX = morphingParams.horizontalShift.toPx() } - }, + }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp) ) { diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/data/Updater.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/data/Updater.kt index 0d498a3d..17d439a4 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/data/Updater.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/data/Updater.kt @@ -113,8 +113,8 @@ object Updater { fun getLatestRelease(channel: Channel): LatestRelease? { return cache.getOrPut(channel) { - if (BuildConfig.DEBUG) { - fetchLatestDebugCI() ?: fetchLatestRelease(channel) + if (BuildConfig.DEBUG && channel == Channel.STABLE) { + fetchLatestDebugCI() ?: fetchLatestRelease(Channel.STABLE) } else { fetchLatestRelease(channel) } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt index e0d9dfa9..1132d061 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt @@ -181,7 +181,6 @@ class FeaturesRootSection : Routes.Route() { } internal fun getRandomizedProfileSnapshot(): String { - context.config.load() return context.config.root.experimental.spoof.randomizeDeviceProfile.currentProfileSnapshot.getNullable() ?.takeIf { it.isNotBlank() } ?: (context.translation["manager.dialogs.randomize_device_profile.empty"] @@ -231,9 +230,12 @@ class FeaturesRootSection : Routes.Route() { ?: error("Failed to read randomized profile backup") val profile = RandomizedDeviceProfile.fromJson(importedJson) val generationToken = UUID.randomUUID().toString() + val profileJson = profile.toJson().toString() + + // Save to local prefs for legacy compatibility context.androidContext.getSharedPreferences("purrfectsnap_spoof", 0) .edit() - .putString("randomized_device_profile", profile.toJson().toString()) + .putString("randomized_device_profile", profileJson) .putString("randomized_device_profile_token", generationToken) .putString("android_id", profile.androidId) .putString("advertising_id", profile.advertisingId) @@ -246,6 +248,8 @@ class FeaturesRootSection : Routes.Route() { val randomizeConfig = context.config.root.experimental.spoof.randomizeDeviceProfile randomizeConfig.profileGenerationToken.set(generationToken) randomizeConfig.currentProfileSnapshot.set(profile.toJson().toString(2)) + randomizeConfig.profileData.set(profileJson) // Shared storage fix + context.config.writeConfig() onConfigChanged() context.shortToast("Randomized profile restored. Restart Snapchat to apply it.") diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/ManageRuleFeature.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/ManageRuleFeature.kt index d3587c3d..d3cc0ab4 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/ManageRuleFeature.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/ManageRuleFeature.kt @@ -4,18 +4,14 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.DeleteSweep -import androidx.compose.material.icons.filled.GroupAdd import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButtonDefaults import androidx.compose.material3.Surface @@ -29,6 +25,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -38,10 +35,10 @@ import androidx.navigation.compose.currentBackStackEntryAsState import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch import me.eternal.purrfectsnap.common.data.MessagingRuleType -import me.eternal.purrfectsnap.ui.manager.rememberRouteScrollState import me.eternal.purrfectsnap.common.data.RuleState import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState import me.eternal.purrfectsnap.common.ui.rememberAsyncUpdateDispatcher +import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableStateList import me.eternal.purrfectsnap.storage.clearRuleIds import me.eternal.purrfectsnap.storage.getRuleIds import me.eternal.purrfectsnap.storage.setRule @@ -140,9 +137,12 @@ class ManageRuleFeature : Routes.Route() { } val updateDispatcher = rememberAsyncUpdateDispatcher() - val currentRuleIds by rememberAsyncMutableState(defaultValue = mutableListOf(), updateDispatcher = updateDispatcher) { + val currentRuleIds = rememberAsyncMutableStateList(defaultValue = emptyList()) { context.database.getRuleIds(currentRuleType.key) } + val currentRuleIdSet = remember(currentRuleIds.size) { + currentRuleIds.toSet() + } fun setRuleState(newState: RuleState?) { ruleState = newState @@ -163,12 +163,12 @@ class ManageRuleFeature : Routes.Route() { fun showAddFriendDialog() { addFriendDialog = AddFriendDialog( context = context, - pinnedIds = currentRuleIds, + pinnedIds = currentRuleIds.toList(), actionHandler = Actions( onFriendState = { friend, state -> context.database.setRule(friend.userId, currentRuleType.key, state) if (state) { - currentRuleIds.add(friend.userId) + if (!currentRuleIdSet.contains(friend.userId)) currentRuleIds.add(friend.userId) } else { currentRuleIds.remove(friend.userId) } @@ -176,16 +176,16 @@ class ManageRuleFeature : Routes.Route() { onGroupState = { group, state -> context.database.setRule(group.conversationId, currentRuleType.key, state) if (state) { - currentRuleIds.add(group.conversationId) + if (!currentRuleIdSet.contains(group.conversationId)) currentRuleIds.add(group.conversationId) } else { currentRuleIds.remove(group.conversationId) } }, getFriendState = { friend -> - currentRuleIds.contains(friend.userId) + currentRuleIdSet.contains(friend.userId) }, getGroupState = { group -> - currentRuleIds.contains(group.conversationId) + currentRuleIdSet.contains(group.conversationId) } ) ) @@ -230,59 +230,62 @@ class ManageRuleFeature : Routes.Route() { title = remember { context.translation[propertyKeyPair.key.propertyName()] }, onBack = { routes.navController.popBackStack() }, modifier = Modifier - .zIndex(2f) + .zIndex(10f) .onGloballyPositioned { val newHeight = with(density) { it.size.height.toDp() } if (newHeight != topBarHeight) topBarHeight = newHeight } ) - Column( + LazyColumn( modifier = Modifier .fillMaxSize() - .padding(top = topBarHeight + 10.dp) - .padding(horizontal = 12.dp, vertical = 10.dp) - .verticalScroll(rememberRouteScrollState(routeInfo.id)), + .padding(top = topBarHeight + 10.dp), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - val headerShape = RoundedCornerShape(22.dp) - Surface( - shape = headerShape, - color = Color.White.copy(alpha = 0.04f), - tonalElevation = 0.dp, - shadowElevation = 0.dp, - border = BorderStroke( - 1.dp, - Brush.linearGradient( - listOf( - PurrfectPalette.glowPrimary.copy(alpha = 0.45f), - PurrfectPalette.glowSecondary.copy(alpha = 0.35f) + item { + val headerShape = RoundedCornerShape(22.dp) + Surface( + shape = headerShape, + color = Color.White.copy(alpha = 0.04f), + tonalElevation = 0.dp, + shadowElevation = 0.dp, + border = BorderStroke( + 1.dp, + Brush.linearGradient( + listOf( + PurrfectPalette.glowPrimary.copy(alpha = 0.45f), + PurrfectPalette.glowSecondary.copy(alpha = 0.35f) + ) ) ) - ) - ) { - Column( - modifier = Modifier - .background(PurrfectPalette.cardOverlay, headerShape) - .padding(horizontal = 16.dp, vertical = 14.dp), - verticalArrangement = Arrangement.spacedBy(6.dp) ) { - Text( - text = context.translation[propertyKeyPair.key.propertyDescription()], - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 16.sp, - color = PurrfectPalette.textSecondary - ) + Column( + modifier = Modifier + .background(PurrfectPalette.cardOverlay, headerShape) + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = context.translation[propertyKeyPair.key.propertyDescription()] ?: "", + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + color = PurrfectPalette.textSecondary + ) + } } } - SelectRuleTypeRadio( - checked = ruleState == null, - text = translation["disable_state_option"], - onStateChanged = { setRuleState(null) } - ) { - Text(text = translation["disable_state_subtext"], fontWeight = FontWeight.Normal, fontSize = 12.sp, color = PurrfectPalette.textSecondary) + item { + SelectRuleTypeRadio( + checked = ruleState == null, + text = translation["disable_state_option"] ?: "Disabled", + onStateChanged = { setRuleState(null) } + ) { + Text(text = translation["disable_state_subtext"] ?: "", fontWeight = FontWeight.Normal, fontSize = 12.sp, color = PurrfectPalette.textSecondary) + } } val manageLabel = when (ruleState) { @@ -291,112 +294,120 @@ class ManageRuleFeature : Routes.Route() { else -> null } - SelectRuleTypeRadio( - checked = ruleState == RuleState.WHITELIST, - text = translation["whitelist_state_option"], - onStateChanged = { setRuleState(RuleState.WHITELIST) } - ) { - Text( - text = translation.format("whitelist_state_subtext", "count" to currentRuleIds.size.toString()), - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - color = PurrfectPalette.textSecondary - ) - Button( - onClick = { showAddFriendDialog() }, - colors = ButtonDefaults.buttonColors( - containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.34f), - contentColor = Color.White + item { + SelectRuleTypeRadio( + checked = ruleState == RuleState.WHITELIST, + text = translation["whitelist_state_option"] ?: "Whitelist", + onStateChanged = { setRuleState(RuleState.WHITELIST) } + ) { + Text( + text = translation.format("whitelist_state_subtext", "count" to currentRuleIds.size.toString()), + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + color = PurrfectPalette.textSecondary ) - ) { - Text(text = translation["whitelist_state_button"]) - } - } - - SelectRuleTypeRadio( - checked = ruleState == RuleState.BLACKLIST, - text = translation["blacklist_state_option"], - onStateChanged = { setRuleState(RuleState.BLACKLIST) } - ) { - Text( - text = translation.format("blacklist_state_subtext", "count" to currentRuleIds.size.toString()), - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - color = PurrfectPalette.textSecondary - ) - Button( - onClick = { showAddFriendDialog() }, - colors = ButtonDefaults.buttonColors( - containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.34f), - contentColor = Color.White - ) - ) { - Text(text = translation["blacklist_state_button"]) - } - } - - Surface( - shape = RoundedCornerShape(22.dp), - color = Color.White.copy(alpha = 0.04f), - tonalElevation = 0.dp, - shadowElevation = 0.dp, - border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f)) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .background(PurrfectPalette.cardOverlay, RoundedCornerShape(22.dp)) - .padding(horizontal = 14.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Surface( - shape = CircleShape, - color = Color.White.copy(alpha = 0.08f), - border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)), - modifier = Modifier.size(46.dp) - ) { - Box( - modifier = Modifier - .fillMaxSize() - .clip(CircleShape) - .background(PurrfectPalette.glowSecondary.copy(alpha = 0.22f)), - contentAlignment = Alignment.Center - ) { - Icon(Icons.Default.DeleteSweep, contentDescription = null, tint = Color.White) - } - } - Column(modifier = Modifier.weight(1f)) { - Text( - text = translation["clear_list_button"], - color = Color.White, - fontWeight = FontWeight.SemiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - if (!manageLabel.isNullOrBlank()) { - Text( - text = manageLabel, - color = PurrfectPalette.textSecondary, - fontSize = 12.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } Button( - onClick = { confirmationDialog = true }, + onClick = { showAddFriendDialog() }, colors = ButtonDefaults.buttonColors( - containerColor = Color.White.copy(alpha = 0.08f), + containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.34f), contentColor = Color.White ) ) { - Text(text = translation["dialog_clear_confirm_button"]) + Text(text = translation["whitelist_state_button"] ?: "Manage") } } } - Spacer(modifier = Modifier.height(routes.bottomPadding)) + item { + SelectRuleTypeRadio( + checked = ruleState == RuleState.BLACKLIST, + text = translation["blacklist_state_option"] ?: "Blacklist", + onStateChanged = { setRuleState(RuleState.BLACKLIST) } + ) { + Text( + text = translation.format("blacklist_state_subtext", "count" to currentRuleIds.size.toString()), + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + color = PurrfectPalette.textSecondary + ) + Button( + onClick = { showAddFriendDialog() }, + colors = ButtonDefaults.buttonColors( + containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.34f), + contentColor = Color.White + ) + ) { + Text(text = translation["blacklist_state_button"] ?: "Manage") + } + } + } + + item { + Surface( + shape = RoundedCornerShape(22.dp), + color = Color.White.copy(alpha = 0.04f), + tonalElevation = 0.dp, + shadowElevation = 0.dp, + border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f)) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(PurrfectPalette.cardOverlay, RoundedCornerShape(22.dp)) + .padding(horizontal = 14.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Surface( + shape = CircleShape, + color = Color.White.copy(alpha = 0.08f), + border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)), + modifier = Modifier.size(46.dp) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(CircleShape) + .background(PurrfectPalette.glowSecondary.copy(alpha = 0.22f)), + contentAlignment = Alignment.Center + ) { + Icon(Icons.Default.DeleteSweep, contentDescription = null, tint = Color.White) + } + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = translation["clear_list_button"] ?: "Clear List", + color = Color.White, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (!manageLabel.isNullOrBlank()) { + Text( + text = manageLabel, + color = PurrfectPalette.textSecondary, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + Button( + onClick = { confirmationDialog = true }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White.copy(alpha = 0.08f), + contentColor = Color.White + ) + ) { + Text(text = translation["dialog_clear_confirm_button"] ?: "Clear") + } + } + } + } + + item { + Spacer(modifier = Modifier.height(routes.bottomPadding)) + } } } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeAbout.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeAbout.kt index 1d061a3b..9176d7da 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeAbout.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeAbout.kt @@ -32,7 +32,6 @@ import me.eternal.purrfectsnap.R import me.eternal.purrfectsnap.ui.manager.Routes import me.eternal.purrfectsnap.ui.manager.ManagerTheme import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette -import me.eternal.purrfectsnap.ui.util.PurrfectMarqueeText import me.eternal.purrfectsnap.ui.util.scaleOnPress class HomeAbout : Routes.Route() { @@ -65,6 +64,7 @@ class HomeAbout : Routes.Route() { name: String, imageRes: Int, avenirNext: FontFamily, + subtitle: String? = null, modifier: Modifier = Modifier ) { val tapSource = remember { MutableInteractionSource() } @@ -85,7 +85,9 @@ class HomeAbout : Routes.Route() { routes.retroGame.navigate() } }, - modifier = modifier.scaleOnPress(tapSource), + modifier = modifier + .height(150.dp) + .scaleOnPress(tapSource), interactionSource = tapSource, shape = RoundedCornerShape(22.dp), color = Color.White.copy(alpha = 0.06f), @@ -94,9 +96,11 @@ class HomeAbout : Routes.Route() { shadowElevation = 0.dp ) { Column( - modifier = Modifier.padding(14.dp), + modifier = Modifier + .fillMaxSize() + .padding(14.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(10.dp) + verticalArrangement = Arrangement.Center ) { Surface( modifier = Modifier.size(64.dp), @@ -111,15 +115,28 @@ class HomeAbout : Routes.Route() { modifier = Modifier.fillMaxSize().clip(CircleShape) ) } - PurrfectMarqueeText( + Text( text = name, color = Color.White, - style = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - fontFamily = avenirNext - ) + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + fontFamily = avenirNext, + textAlign = androidx.compose.ui.text.style.TextAlign.Center, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() ) + subtitle?.takeIf { it.isNotBlank() }?.let { + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = it, + color = PurrfectPalette.textSecondary, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } } } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeRootSection.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeRootSection.kt index 64b4ba71..313c4e89 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeRootSection.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeRootSection.kt @@ -127,6 +127,8 @@ import me.eternal.purrfectsnap.storage.getQuickTiles import me.eternal.purrfectsnap.storage.setQuickTiles import me.eternal.purrfectsnap.ui.manager.Routes import me.eternal.purrfectsnap.ui.manager.ManagerTheme +import me.eternal.purrfectsnap.ui.manager.ManagerAssistantEntry +import me.eternal.purrfectsnap.ui.manager.ManagerAssistantTriggerStyle import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette import me.eternal.purrfectsnap.ui.manager.data.UpdateDownloader import me.eternal.purrfectsnap.ui.manager.data.Updater @@ -271,27 +273,31 @@ class HomeRootSection : Routes.Route() { icon: ImageVector, label: String? = null, contentDescription: String? = label, + modifier: Modifier = Modifier, onClick: () -> Unit, ) { Surface( + modifier = modifier.height(36.dp), shape = RoundedCornerShape(40), color = Color.White.copy(alpha = 0.06f), border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)) ) { Row( modifier = Modifier + .fillMaxWidth() .clip(RoundedCornerShape(40)) .clickable(onClick = onClick) - .padding(horizontal = 14.dp, vertical = 8.dp), + .padding(horizontal = 10.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.Center ) { - Icon(icon, contentDescription = contentDescription, tint = Color.White) + Icon(icon, contentDescription = contentDescription, tint = Color.White, modifier = Modifier.size(20.dp)) label?.let { + Spacer(modifier = Modifier.width(6.dp)) Text( text = it, color = Color.White, - fontSize = 13.sp, + fontSize = 12.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -323,13 +329,20 @@ class HomeRootSection : Routes.Route() { @Composable private fun RowScope.HomeActionChips() { + ManagerAssistantEntry( + context = context, + routes = routes, + style = ManagerAssistantTriggerStyle.DEFAULT + ) TopBarActionChip( icon = Icons.Filled.BugReport, - label = context.translation["manager.routes.home_logs"] + label = context.translation["manager.routes.home_logs"], + modifier = Modifier ) { routes.homeLogs.navigate() } TopBarActionChip( icon = Icons.Filled.Info, - label = translation["manager.routes.home_about"] + label = translation["manager.routes.home_about"], + modifier = Modifier ) { routes.about.navigate() } } @@ -733,9 +746,8 @@ class HomeRootSection : Routes.Route() { Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.wrapContentWidth(), ) { - Spacer(modifier = Modifier.weight(1f)) HomeActionChips() } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeSettings.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeSettings.kt index f174213e..0df68729 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeSettings.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/home/HomeSettings.kt @@ -75,34 +75,9 @@ class HomeSettings : Routes.Route() { internal fun scheduleUpdateCheck() { val workManager = WorkManager.getInstance(context.androidContext) val updateSettings = context.config.root.global.updateSettings - var configDirty = false - val autoUpdateCheck = updateSettings.autoUpdateCheck.getNullable() ?: run { - configDirty = true - updateSettings.autoUpdateCheck.set(true) - true - } - val frequency = updateSettings.updateCheckFrequency.getNullable() ?: run { - configDirty = true - updateSettings.updateCheckFrequency.set("daily") - "daily" - } - val updateChannel = updateSettings.updateChannel.getNullable() ?: run { - configDirty = true - updateSettings.updateChannel.set("stable") - "stable" - } - if (configDirty) { - context.config.writeConfig() - } + val autoUpdateCheck = updateSettings.autoUpdateCheck.get() if (autoUpdateCheck) { - val repeatInterval = when (frequency) { - "daily" -> 1L - "weekly" -> 7L - "monthly" -> 30L - else -> 1L - } - val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() @@ -112,10 +87,10 @@ class HomeSettings : Routes.Route() { .putString("channel_description", translation["update_notification_channel_description"]) .putString("notification_title", translation["update_notification_title"]) .putString("notification_text", translation["update_notification_text"]) - .putString("update_channel", updateChannel) + .putString("update_channel", "stable") .build() - val workRequest = PeriodicWorkRequestBuilder(repeatInterval, TimeUnit.DAYS) + val workRequest = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) .setConstraints(constraints) .setInputData(inputData) .build() diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ManageScriptReposSection.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ManageScriptReposSection.kt index 26107c5f..5ff97258 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ManageScriptReposSection.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ManageScriptReposSection.kt @@ -279,8 +279,9 @@ class ManageScriptReposSection : Routes.Route() { } override val content: @Composable (androidx.navigation.NavBackStackEntry) -> Unit = { - val repositories by remember(refreshTrigger.value) { - mutableStateOf>(runBlocking { context.database.getRepositories("script") }) + var repositories by remember { mutableStateOf>(emptyList()) } + LaunchedEffect(refreshTrigger.value) { + repositories = context.database.getRepositories("script") } val density = LocalDensity.current val statusBarTopPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/AddFriendDialog.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/AddFriendDialog.kt index 678d3047..4bf8b687 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/AddFriendDialog.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/AddFriendDialog.kt @@ -26,9 +26,11 @@ import kotlinx.coroutines.* import me.eternal.purrfectsnap.RemoteSideContext import me.eternal.purrfectsnap.common.data.MessagingFriendInfo import me.eternal.purrfectsnap.common.data.MessagingGroupInfo +import me.eternal.purrfectsnap.common.data.MessagingRuleType import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie import me.eternal.purrfectsnap.storage.getFriends import me.eternal.purrfectsnap.storage.getGroups +import me.eternal.purrfectsnap.storage.getRuleIds import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette import me.eternal.purrfectsnap.ui.util.coil.BitmojiImage @@ -223,32 +225,34 @@ class AddFriendDialog( friends: List, groups: List ) { - cachedFriends = friends.run { - if (pinnedIds != null) { - sortedBy { -pinnedIds.indexOf(it.userId) } - } else { - this + coroutineScope.launch(Dispatchers.IO) { + val sortedFriends = context.sortSocialFriends(friends, pinnedIds = pinnedIds) + val sortedGroups = groups.run { + if (pinnedIds != null) { + sortedBy { -pinnedIds.indexOf(it.conversationId) } + } else { + // Priority sort for whitelisted groups + val whitelistedIds = context.database.getRuleIds(MessagingRuleType.STEALTH.key).toSet() + sortedWith { a, b -> + val aSelected = whitelistedIds.contains(a.conversationId) + val bSelected = whitelistedIds.contains(b.conversationId) + if (aSelected != bSelected) if (aSelected) -1 else 1 + else a.name.compareTo(b.name, ignoreCase = true) + } + } } - } - cachedGroups = groups.run { - if (pinnedIds != null) { - sortedBy { -pinnedIds.indexOf(it.conversationId) } - } else { - this + withContext(Dispatchers.Main) { + cachedFriends = sortedFriends + cachedGroups = sortedGroups + if (friends.isNotEmpty() || groups.isNotEmpty()) { + timeoutJob?.cancel() + hasFetchError = false + } } } - if (friends.isNotEmpty() || groups.isNotEmpty()) { - timeoutJob?.cancel() - hasFetchError = false - } - } - - val updateSnapshot: (List, List) -> Unit = { friends, groups -> - coroutineScope.launch { - applySnapshot(friends, groups) - } } + // Initial database load withContext(Dispatchers.IO) { applySnapshot( context.database.getFriends(descOrder = true), @@ -256,20 +260,11 @@ class AddFriendDialog( ) } - context.database.receiveMessagingDataCallback = updateSnapshot + // Real-time synchronization flow context.requestSocialSnapshotRefresh() - - coroutineScope.launch(Dispatchers.IO) { - repeat(25) { - delay(1000) - val dbFriends = context.database.getFriends(descOrder = true) - val dbGroups = context.database.getGroups() - if (dbFriends.isNotEmpty() || dbGroups.isNotEmpty()) { - withContext(Dispatchers.Main) { - applySnapshot(dbFriends, dbGroups) - } - return@launch - } + coroutineScope.launch { + context.database.messagingDataFlow.collect { (friends, groups) -> + applySnapshot(friends, groups) } } @@ -286,7 +281,6 @@ class AddFriendDialog( onDispose { timeoutJob?.cancel() context.bridgeService?.clearEphemeralSocialSnapshotRequest() - context.database.receiveMessagingDataCallback = { _, _ -> } } } @@ -346,6 +340,7 @@ class AddFriendDialog( it.mutableUsername.contains(searchKeyword.value, ignoreCase = true) || it.displayName?.contains(searchKeyword.value, ignoreCase = true) == true } ?: cachedFriends!! + val selectedFriendCount by remember(filteredFriends) { derivedStateOf { filteredFriends.count { friend -> @@ -356,6 +351,16 @@ class AddFriendDialog( val hasFriendsSelected = selectedFriendCount > 0 val allFriendsSelected = filteredFriends.isNotEmpty() && selectedFriendCount == filteredFriends.size + val selectedGroupCount by remember(filteredGroups) { + derivedStateOf { + filteredGroups.count { group -> + stateCache[group.conversationId] ?: actionHandler.getGroupState(group) + } + } + } + val hasGroupsSelected = selectedGroupCount > 0 + val allGroupsSelected = filteredGroups.isNotEmpty() && selectedGroupCount == filteredGroups.size + DialogHeader(searchKeyword) LazyColumn( @@ -365,14 +370,54 @@ class AddFriendDialog( ) { item { if (filteredGroups.isNotEmpty()) { - Text( - text = translation["category_groups"], - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, + Row( modifier = Modifier + .fillMaxWidth() .padding(bottom = 8.dp, top = 8.dp), - color = Color.White - ) + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = translation["category_groups"], + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = Color.White + ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + TextButton( + onClick = { + coroutineScope.launch(Dispatchers.IO) { + filteredGroups.forEach { group -> + stateCache[group.conversationId] = true + actionHandler.onGroupState(group, true) + } + } + }, + enabled = !allGroupsSelected + ) { + Text( + text = context.translation["manager.dialogs.messaging_action.select_all_button"], + color = if (allGroupsSelected) Color.White.copy(alpha = 0.45f) else PurrfectPalette.glowSecondary + ) + } + TextButton( + onClick = { + coroutineScope.launch(Dispatchers.IO) { + filteredGroups.forEach { group -> + stateCache[group.conversationId] = false + actionHandler.onGroupState(group, false) + } + } + }, + enabled = hasGroupsSelected + ) { + Text( + text = translation["unselect_all_button"], + color = if (hasGroupsSelected) PurrfectPalette.glowPrimary else Color.White.copy(alpha = 0.45f) + ) + } + } + } } } @@ -417,11 +462,7 @@ class AddFriendDialog( ) { Text( text = context.translation["manager.dialogs.messaging_action.select_all_button"], - color = if (allFriendsSelected) { - Color.White.copy(alpha = 0.45f) - } else { - PurrfectPalette.glowSecondary - } + color = if (allFriendsSelected) Color.White.copy(alpha = 0.45f) else PurrfectPalette.glowSecondary ) } TextButton( @@ -437,11 +478,7 @@ class AddFriendDialog( ) { Text( text = translation["unselect_all_button"], - color = if (hasFriendsSelected) { - PurrfectPalette.glowPrimary - } else { - Color.White.copy(alpha = 0.45f) - } + color = if (hasFriendsSelected) PurrfectPalette.glowPrimary else Color.White.copy(alpha = 0.45f) ) } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/LoggedStories.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/LoggedStories.kt index c2340729..540d56c9 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/LoggedStories.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/LoggedStories.kt @@ -35,6 +35,7 @@ import me.eternal.purrfectsnap.storage.getFriendInfo import me.eternal.purrfectsnap.ui.manager.Routes import me.eternal.purrfectsnap.ui.util.Dialog import me.eternal.purrfectsnap.ui.util.coil.ImageRequestHelper +import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState import java.io.File import java.text.DateFormat import java.util.Date @@ -44,12 +45,11 @@ import kotlin.math.absoluteValue class LoggedStories : Routes.Route() { override val title: @Composable () -> Unit = { val navBackStackEntry by routes.navController.currentBackStackEntryAsState() - val text = remember(navBackStackEntry) { - navBackStackEntry?.arguments?.getString("id")?.let { - context.database.getFriendInfo(it)?.displayName - } + val userId = navBackStackEntry?.arguments?.getString("id") + val displayName by rememberAsyncMutableState(defaultValue = null) { + userId?.let { context.database.getFriendInfo(it)?.displayName } } - text?.let { Text(it, maxLines = 1, overflow = TextOverflow.Ellipsis) } + displayName?.let { Text(it, maxLines = 1, overflow = TextOverflow.Ellipsis) } } @OptIn(ExperimentalCoilApi::class, ExperimentalLayoutApi::class) @@ -57,7 +57,9 @@ class LoggedStories : Routes.Route() { val userId = navBackStackEntry.arguments?.getString("id") ?: return@content val stories = remember { mutableStateListOf() } - val friendInfo = remember { context.database.getFriendInfo(userId) } + val friendInfo by rememberAsyncMutableState(defaultValue = null) { + context.database.getFriendInfo(userId) + } var lastStoryTimestamp by remember { mutableLongStateOf(Long.MAX_VALUE) } var selectedStory by remember { mutableStateOf(null) } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/SocialFriendSorting.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/SocialFriendSorting.kt new file mode 100644 index 00000000..34865317 --- /dev/null +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/SocialFriendSorting.kt @@ -0,0 +1,30 @@ +package me.eternal.purrfectsnap.ui.manager.pages.social + +import me.eternal.purrfectsnap.RemoteSideContext +import me.eternal.purrfectsnap.common.data.MessagingFriendInfo +import me.eternal.purrfectsnap.storage.getFriends + +internal fun RemoteSideContext.sortSocialFriends( + friends: List, + pinnedIds: List? = null +): List { + val whitelistedIds = pinnedIds?.toSet() ?: database.getFriends().map { it.userId }.toSet() + val sortByStreakLength = config.root.userInterface.sortSocialTabByStreakLength.get() + + return friends.sortedWith { a, b -> + val aSelected = whitelistedIds.contains(a.userId) + val bSelected = whitelistedIds.contains(b.userId) + + if (aSelected != bSelected) { + return@sortedWith if (aSelected) -1 else 1 + } + + if (sortByStreakLength) { + val aStreak = a.streaks?.length ?: 0 + val bStreak = b.streaks?.length ?: 0 + if (aStreak != bStreak) return@sortedWith bStreak.compareTo(aStreak) + } + + (a.displayName ?: a.mutableUsername).compareTo(b.displayName ?: b.mutableUsername, ignoreCase = true) + } +} diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/SocialRootSection.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/SocialRootSection.kt index 891f4c66..c7fb2d97 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/SocialRootSection.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/SocialRootSection.kt @@ -35,8 +35,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavBackStackEntry -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import me.eternal.purrfectsnap.R import me.eternal.purrfectsnap.common.data.MessagingFriendInfo import me.eternal.purrfectsnap.common.data.MessagingGroupInfo @@ -53,10 +52,32 @@ class SocialRootSection : Routes.Route() { internal var friendList: List by mutableStateOf(emptyList()) internal var groupList: List by mutableStateOf(emptyList()) - internal fun updateScopeLists() { - context.coroutineScope.launch { - friendList = context.database.getFriends(descOrder = true) - groupList = context.database.getGroups() + @Composable + fun SocialDataController() { + LaunchedEffect(Unit) { + // Initial data fetch from the database + withContext(Dispatchers.IO) { + val dbFriends = context.database.getFriends(descOrder = true) + val dbGroups = context.database.getGroups() + val sortedFriends = context.sortSocialFriends(dbFriends) + withContext(Dispatchers.Main) { + friendList = sortedFriends + groupList = dbGroups + } + } + + // Real-time synchronization from the bridge + context.database.messagingDataFlow.collect { + withContext(Dispatchers.IO) { + val dbFriends = context.database.getFriends(descOrder = true) + val dbGroups = context.database.getGroups() + val sortedFriends = context.sortSocialFriends(dbFriends) + withContext(Dispatchers.Main) { + friendList = sortedFriends + groupList = dbGroups + } + } + } } } @@ -124,11 +145,6 @@ class SocialRootSection : Routes.Route() { addFriendDialog?.Content { addFriendDialog = null } - DisposableEffect(Unit) { - onDispose { - updateScopeLists() - } - } } FloatingActionButton( @@ -158,8 +174,7 @@ class SocialRootSection : Routes.Route() { }, getFriendState = { friend -> context.database.getFriendInfo(friend.userId) != null }, getGroupState = { group -> context.database.getGroupInfo(group.conversationId) != null } - ), - pinnedIds = (friendList.map { it.userId } + groupList.map { it.conversationId }).reversed(), + ) ) }, modifier = Modifier diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionAboutView.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionAboutView.kt index beb32cb3..b8753bfb 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionAboutView.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionAboutView.kt @@ -135,13 +135,26 @@ fun HomeAbout.AphelionAboutScreen(nav: NavBackStackEntry) { color = Color.White, modifier = Modifier.padding(bottom = 4.dp) ) - Row( + Column( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically + verticalArrangement = Arrangement.spacedBy(12.dp) ) { - DeveloperCard(name = "ΞTΞRNAL", imageRes = R.drawable.pfp_external, avenirNext = avenirNext, modifier = Modifier.weight(1f)) - DeveloperCard(name = "", imageRes = R.drawable.pfp_rsr, avenirNext = avenirNext, modifier = Modifier.weight(1f)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically + ) { + DeveloperCard(name = "Eternal", subtitle = "", imageRes = R.drawable.pfp_external, avenirNext = avenirNext, modifier = Modifier.weight(1f)) + DeveloperCard(name = "Kaladin", subtitle = "", imageRes = R.drawable.pfp_kaladin, avenirNext = avenirNext, modifier = Modifier.weight(1f)) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically + ) { + DeveloperCard(name = "schrodingerspet", subtitle = "", imageRes = R.drawable.pfp_schrodingerspet, avenirNext = avenirNext, modifier = Modifier.weight(1f)) + DeveloperCard(name = "RSR", subtitle = "", imageRes = R.drawable.pfp_rsr, avenirNext = avenirNext, modifier = Modifier.weight(1f)) + } } } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionHomeView.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionHomeView.kt index 590e1732..e2c1d07f 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionHomeView.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionHomeView.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.lerp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex @@ -63,6 +64,8 @@ import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog import me.eternal.purrfectsnap.ui.manager.data.UpdateDownloader import me.eternal.purrfectsnap.ui.manager.data.Updater import me.eternal.purrfectsnap.ui.manager.data.Updater.Channel +import me.eternal.purrfectsnap.ui.manager.ManagerAssistantEntry +import me.eternal.purrfectsnap.ui.manager.ManagerAssistantTriggerStyle import me.eternal.purrfectsnap.ui.manager.pages.home.HomeRootSection import me.eternal.purrfectsnap.ui.manager.pages.home.QuickActionsDialog import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette @@ -148,11 +151,15 @@ fun HomeRootSection.AphelionHomeScreen(nav: NavBackStackEntry) { label: String? = null, contentDescription: String? = label, shrinkFactor: Float = 1f, + modifier: Modifier = Modifier, + expandedWidth: Dp? = null, + collapsedWidth: Dp = 36.dp, haptic: HapticFeedback, onClick: () -> Unit, ) { + val targetWidth = expandedWidth?.let { lerp(collapsedWidth, it, shrinkFactor) } Surface( - modifier = Modifier.height(36.dp).widthIn(min = 36.dp), + modifier = modifier.height(36.dp).then(if (targetWidth != null) Modifier.width(targetWidth) else Modifier), shape = RoundedCornerShape(40), color = Color.White.copy(alpha = 0.06f), border = BorderStroke( @@ -167,9 +174,10 @@ fun HomeRootSection.AphelionHomeScreen(nav: NavBackStackEntry) { ) { Row( modifier = Modifier + .fillMaxWidth() .clip(RoundedCornerShape(40)) .clickable { haptic.performHapticFeedback(HapticFeedbackType.LongPress); onClick() } - .padding(vertical = 6.dp, horizontal = lerp(10.dp, 12.dp, shrinkFactor)), + .padding(vertical = 6.dp, horizontal = lerp(7.dp, 10.dp, shrinkFactor)), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { @@ -182,17 +190,19 @@ fun HomeRootSection.AphelionHomeScreen(nav: NavBackStackEntry) { } ) if (label != null) { - val labelAlpha = (shrinkFactor - 0.1f).coerceIn(0f, 1f) - Spacer(modifier = Modifier.width((8 * shrinkFactor).dp)) - Text( - text = label, - color = Color.White.copy(alpha = labelAlpha), - fontSize = 12.sp, fontWeight = FontWeight.Medium, - maxLines = 1, overflow = TextOverflow.Clip, - modifier = Modifier - .graphicsLayer { alpha = labelAlpha; translationX = (-4 * (1f - shrinkFactor)).dp.toPx() } - .widthIn(max = (75 * shrinkFactor).dp) - ) + val labelAlpha = ((shrinkFactor - 0.45f) / 0.55f).coerceIn(0f, 1f) + if (labelAlpha > 0.02f) { + Spacer(modifier = Modifier.width((8 * shrinkFactor).dp)) + Text( + text = label, + color = Color.White.copy(alpha = labelAlpha), + fontSize = 12.sp, fontWeight = FontWeight.Medium, + maxLines = 1, overflow = TextOverflow.Ellipsis, + modifier = Modifier + .graphicsLayer { alpha = labelAlpha; translationX = (-4 * (1f - shrinkFactor)).dp.toPx() } + .weight(1f, fill = false) + ) + } } } } @@ -206,14 +216,23 @@ fun HomeRootSection.AphelionHomeScreen(nav: NavBackStackEntry) { val shrinkFactor by remember(scrollState.value) { derivedStateOf { (1f - (scrollState.value.toFloat() / Motion.HEADER_MORPH_THRESHOLD)).coerceIn(0f, 1f) } } + ManagerAssistantEntry( + context = context, + routes = routes, + style = ManagerAssistantTriggerStyle.APHELION, + shrinkFactor = shrinkFactor, + modifier = Modifier.width(lerp(36.dp, 66.dp, shrinkFactor)) + ) AphelionTopBarActionChip( icon = Icons.Filled.BugReport, label = context.translation["manager.routes.home_logs"], + expandedWidth = 88.dp, shrinkFactor = shrinkFactor, haptic = haptic ) { routes.homeLogs.navigate() } AphelionTopBarActionChip( icon = Icons.Filled.Settings, label = context.translation["manager.routes.home_settings"], + expandedWidth = 96.dp, shrinkFactor = shrinkFactor, haptic = haptic ) { routes.settings.navigate() } } @@ -226,7 +245,6 @@ fun HomeRootSection.AphelionHomeScreen(nav: NavBackStackEntry) { downloadState: UpdateDownloader.DownloadState, downloadProgress: Float, onUpdateAction: () -> Unit, - channelLabel: String, isPurrAuraActive: Boolean, onAboutClick: () -> Unit, avenirNext: FontFamily, @@ -283,7 +301,7 @@ fun HomeRootSection.AphelionHomeScreen(nav: NavBackStackEntry) { horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), verticalArrangement = Arrangement.spacedBy(10.dp) ) { - HeroBadge(translation.format("hero_version_label", "version" to versionName, "channel" to channelLabel)) + HeroBadge(translation.format("hero_version_label", "version" to versionName)) gitHashShort.takeIf { it.isNotBlank() && it.lowercase() != "unknown" }?.let { HeroBadge(translation.format("hero_build_label", "build" to it)) } @@ -444,17 +462,17 @@ fun HomeRootSection.AphelionHomeScreen(nav: NavBackStackEntry) { } hasInitialized -> storedTiles else -> { - context.database.setQuickTiles(allQuickTileNames) - prefs.edit().putBoolean(HomeRootSection.QUICK_TILES_INITIALIZED_PREF, true).apply() + context.coroutineScope.launch(Dispatchers.IO) { + context.database.setQuickTiles(allQuickTileNames) + prefs.edit().putBoolean(HomeRootSection.QUICK_TILES_INITIALIZED_PREF, true).apply() + } allQuickTileNames } } } - val updateChannel = context.config.root.global.updateSettings.updateChannel.getNullable() ?: "stable" - val channelLabel = if (updateChannel == "prerelease") translation["channel_label_prerelease"] ?: "" else translation["channel_label_stable"] ?: "" - val latestUpdate by rememberAsyncMutableState(defaultValue = null, keys = arrayOf(updateChannel)) { - Updater.getLatestRelease(if (updateChannel == "prerelease") Channel.PRERELEASE else Channel.STABLE) + val latestUpdate by rememberAsyncMutableState(defaultValue = null) { + Updater.getLatestRelease(Channel.STABLE) } val downloadState by UpdateDownloader.downloadState.collectAsState() val downloadProgress by UpdateDownloader.downloadProgress.collectAsState() @@ -502,7 +520,7 @@ fun HomeRootSection.AphelionHomeScreen(nav: NavBackStackEntry) { changelogLoading = true changelogError = null coroutineScope.launch(Dispatchers.IO) { - val url = if (updateChannel == "prerelease") changelogPrereleaseUrl else changelogStableUrl + val url = changelogStableUrl runCatching { OkHttpClient().newCall(Request.Builder().url(url).build()).execute().use { response -> val body = response.body?.string() ?: throw IllegalStateException("Empty body") @@ -540,7 +558,7 @@ fun HomeRootSection.AphelionHomeScreen(nav: NavBackStackEntry) { fullChangelogLoading = true fullChangelogError = null coroutineScope.launch(Dispatchers.IO) { - val url = if (updateChannel == "prerelease") changelogPrereleaseUrl else changelogStableUrl + val url = changelogStableUrl runCatching { OkHttpClient().newCall(Request.Builder().url(url).build()).execute().use { response -> val body = response.body?.string() ?: throw IllegalStateException("Empty body") @@ -646,37 +664,27 @@ fun HomeRootSection.AphelionHomeScreen(nav: NavBackStackEntry) { .padding(horizontal = 16.dp) .height(headerHeight) ) { - Text( - text = "PurrfectSnap", - color = Color.White.copy(alpha = stickyBrandingAlpha), - fontSize = 18.sp, fontWeight = FontWeight.Bold, fontFamily = avenirNext, - modifier = Modifier.align(Alignment.Center) - ) - val announcementShift by remember(focusFactor) { derivedStateOf { (-6 * focusFactor).dp } } Row( - modifier = Modifier.align(Alignment.CenterStart).graphicsLayer { translationX = announcementShift.toPx() }, + modifier = Modifier + .align(Alignment.Center) + .wrapContentWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) + horizontalArrangement = Arrangement.spacedBy(6.dp) ) { AphelionTopBarActionChip( icon = Icons.Filled.Notifications, label = null, + expandedWidth = 52.dp, shrinkFactor = (1f - focusFactor).coerceIn(0f, 1f), contentDescription = translation["announcements_button_description"], haptic = haptic ) { showAnnouncementsDialog = true; loadAnnouncements() } AphelionTopBarActionChip( icon = Icons.Filled.Description, label = null, + expandedWidth = 52.dp, shrinkFactor = (1f - focusFactor).coerceIn(0f, 1f), contentDescription = translation.getOrNull("changelog_button_description") ?: "Open full changelog", haptic = haptic ) { showFullChangelogDialog = true; loadFullChangelog() } - } - val settingsShift by remember(focusFactor) { derivedStateOf { (6 * focusFactor).dp } } - Row( - modifier = Modifier.align(Alignment.CenterEnd).graphicsLayer { translationX = settingsShift.toPx() }, - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically - ) { AphelionHomeActionChips(scrollState = scrollState, haptic = haptic) } } @@ -692,7 +700,6 @@ fun HomeRootSection.AphelionHomeScreen(nav: NavBackStackEntry) { downloadState = downloadState, downloadProgress = downloadProgress, onUpdateAction = { latestUpdate?.let { showChangelogDialog = true; loadChangelog() } }, - channelLabel = channelLabel, isPurrAuraActive = isPurrAuraActive, onAboutClick = { routes.about.navigate() }, avenirNext = avenirNext, diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionLogsView.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionLogsView.kt index 32ada622..dcfc434f 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionLogsView.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionLogsView.kt @@ -47,15 +47,11 @@ fun HomeLogs.AphelionLogsScreen(nav: NavBackStackEntry) { isRefreshing = true coroutineScope.launch(Dispatchers.IO) { val readerResult = runCatching { - context.log.newReader { line -> - if (shouldHideLog(line)) return@newReader - coroutineScope.launch(Dispatchers.Main) { - visibleLogs.add(line) - } - } + context.log.newReader { /* items are batch-added from reader logic below */ } } readerResult.onFailure { context.longToast(translation["read_logs_failed_toast"] ?: "Failed to read logs") + withContext(Dispatchers.Main) { isRefreshing = false } } readerResult.getOrNull()?.let { reader -> logReader = reader @@ -78,52 +74,63 @@ fun HomeLogs.AphelionLogsScreen(nav: NavBackStackEntry) { fun LogFilterDialog() { Dialog(onDismissRequest = { showFilterDialog = false }) { PurrfectOverlayTheme { - PurrfectGlassCard(title = translation["filter_logs_title"] ?: "Filter Log Categories", modifier = Modifier.fillMaxWidth()) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - HomeLogs.LogCategory.entries.forEach { category -> - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .clickable { - enabledCategories.keys.forEach { enabledCategories[it] = false } - enabledCategories[category] = true - refreshLogs() + PurrfectGlassCard( + title = translation["filter_logs_title"] ?: "Log Filters", + modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(14.dp), + color = Color.White.copy(alpha = 0.08f), + border = BorderStroke(1.dp, Color.White.copy(alpha = 0.05f)) + ) { + Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + HomeLogs.LogCategory.entries.forEach { category -> + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickable { + enabledCategories[category] = !(enabledCategories[category] ?: true) + refreshLogs() + } + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = enabledCategories[category] == true, + onCheckedChange = { checked -> + enabledCategories[category] = checked + refreshLogs() + }, + colors = CheckboxDefaults.colors( + checkedColor = PurrfectPalette.glowPrimary, + uncheckedColor = Color.White.copy(alpha = 0.3f), + checkmarkColor = Color.White + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = translation[category.translationKey] ?: category.name, + color = Color.White, + style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold) + ) } - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Checkbox( - checked = enabledCategories[category] == true, - onCheckedChange = { checked -> - enabledCategories[category] = checked - refreshLogs() - }, - colors = CheckboxDefaults.colors( - checkedColor = PurrfectPalette.glowPrimary, - uncheckedColor = Color.White.copy(alpha = 0.4f), - checkmarkColor = Color.White - ) - ) - Text( - text = translation[category.translationKey] ?: category.name, - color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.Medium - ) + } } } - - Spacer(modifier = Modifier.height(8.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - Button( - onClick = { showFilterDialog = false }, - shape = RoundedCornerShape(14.dp), - colors = ButtonDefaults.buttonColors(containerColor = PurrfectPalette.glowPrimary) - ) { - Text(translation["filter_logs_done_button"] ?: "Done") - } + + Button( + onClick = { showFilterDialog = false }, + modifier = Modifier.fillMaxWidth().height(54.dp), + shape = RoundedCornerShape(18.dp), + colors = ButtonDefaults.buttonColors(containerColor = PurrfectPalette.glowPrimary) + ) { + Text(translation["filter_logs_done_button"] ?: "Apply Filters", fontWeight = FontWeight.Bold, fontSize = 16.sp) } } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionSettingsView.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionSettingsView.kt index 861af766..5fc222b9 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionSettingsView.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionSettingsView.kt @@ -2,6 +2,7 @@ package me.eternal.purrfectsnap.ui.manager.pages.themes.aphelion import android.content.SharedPreferences import android.content.Intent +import com.google.gson.JsonParser import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -29,18 +30,31 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.core.content.edit import androidx.core.net.toUri import androidx.navigation.NavBackStackEntry +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import me.eternal.purrfectsnap.R import me.eternal.purrfectsnap.common.action.EnumAction import me.eternal.purrfectsnap.common.bridge.InternalFileHandleType +import me.eternal.purrfectsnap.common.bridge.wrapper.LoggerConversationExportTarget +import me.eternal.purrfectsnap.common.bridge.wrapper.LoggedMessage +import me.eternal.purrfectsnap.common.data.ContentType import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState +import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader +import me.eternal.purrfectsnap.core.features.impl.downloader.decoder.DecodedAttachment +import me.eternal.purrfectsnap.core.features.impl.downloader.decoder.MessageDecoder +import me.eternal.purrfectsnap.core.wrapper.impl.getMessageText +import me.eternal.purrfectsnap.storage.findFriend import me.eternal.purrfectsnap.storage.getAllScopeNotes +import me.eternal.purrfectsnap.storage.getFriendInfo +import me.eternal.purrfectsnap.storage.getGroupInfo import me.eternal.purrfectsnap.storage.setAllScopeNotes import me.eternal.purrfectsnap.ui.manager.Routes import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog @@ -60,6 +74,8 @@ import androidx.compose.ui.platform.LocalView import androidx.core.view.drawToBitmap import java.io.File import java.net.URLEncoder +import java.text.DateFormat +import java.util.Date @OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable @@ -222,22 +238,12 @@ fun HomeSettings.AphelionSettingsScreen(nav: NavBackStackEntry) { RowTitle(title = translation["updates_title"]) Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { var autoUpdateCheck by remember { mutableStateOf(context.config.root.global.updateSettings.autoUpdateCheck.getNullable() ?: true) } - var selectedChannel by remember { mutableStateOf(context.config.root.global.updateSettings.updateChannel.getNullable() ?: "stable") } - var channelMenuExpanded by remember { mutableStateOf(false) } ShiftedRow { Row(modifier = Modifier.fillMaxWidth().heightIn(min = 55.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { Text(text = translation["auto_update_check"], fontSize = 14.sp) Switch(checked = autoUpdateCheck, onCheckedChange = { if (context.config.root.global.uiSettings.hapticFeedback.get()) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress); autoUpdateCheck = it; context.config.root.global.updateSettings.autoUpdateCheck.set(it); context.config.writeConfig(); scheduleUpdateCheck() }, modifier = Modifier.padding(end = 26.dp), colors = purrfectSwitchColors()) } } - AnimatedVisibility(visible = autoUpdateCheck) { - ExposedDropdownMenuBox(expanded = channelMenuExpanded, onExpandedChange = { channelMenuExpanded = it }, modifier = Modifier.fillMaxWidth().padding(horizontal = 26.dp)) { - AestheticDropdownField(value = translation.getOrNull("update_channel_${selectedChannel}") ?: selectedChannel, expanded = channelMenuExpanded, modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable), onClick = { channelMenuExpanded = true }) - ExposedDropdownMenu(expanded = channelMenuExpanded, onDismissRequest = { channelMenuExpanded = false }) { - listOf("stable", "prerelease").forEach { channel -> DropdownMenuItem(text = { Text(text = translation.getOrNull("update_channel_${channel}") ?: channel) }, onClick = { selectedChannel = channel; channelMenuExpanded = false; context.config.root.global.updateSettings.updateChannel.set(channel); context.config.writeConfig(); scheduleUpdateCheck() }) } - } - } - } } } @@ -257,11 +263,494 @@ fun HomeSettings.AphelionSettingsScreen(nav: NavBackStackEntry) { var storedMessagesCount by rememberAsyncMutableState(defaultValue = 0) { context.messageLogger.getStoredMessageCount() } var storedStoriesCount by rememberAsyncMutableState(defaultValue = 0) { context.messageLogger.getStoredStoriesCount() } var showImportDialog by remember { mutableStateOf(false) } + var showExportOptionsDialog by remember { mutableStateOf(false) } + var showConversationExportDialog by remember { mutableStateOf(false) } + var showConversationFormatDialog by remember { mutableStateOf(false) } + var conversationSearchQuery by remember { mutableStateOf("") } + var selectedConversationForExport by remember { mutableStateOf(null) } + var pendingConversationExportTarget by remember { mutableStateOf(null) } + val loggerHistoryTranslation = remember { context.translation.getCategory("logger_history") } + + data class ConversationSearchTarget( + val target: LoggerConversationExportTarget, + val friendDisplayName: String?, + val friendUsername: String?, + val chatDisplayName: String?, + val groupDisplayName: String?, + val readableUsernames: List, + val readableIdentifiers: List, + val isDirectChat: Boolean, + val isGroupChat: Boolean, + val sortOrder: Int + ) + + data class ConversationExportFormat( + val extension: String, + val mimeType: String, + val label: String + ) + + data class ParsedConversationMessage( + val senderId: String, + val senderUsername: String, + val timestamp: Long, + val contentType: ContentType, + val messageText: String?, + val attachments: List + ) + + fun String.isUuidLike(): Boolean { + val value = trim() + if (value.length != 36) return false + if (value[8] != '-' || value[13] != '-' || value[18] != '-' || value[23] != '-') return false + return value.filterIndexed { index, _ -> + index != 8 && index != 13 && index != 18 && index != 23 + }.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' } + } + + fun String.isLikelyInternalId(): Boolean { + val value = trim() + if (value.isUuidLike()) return true + if (value.length >= 10 && value.all(Char::isDigit)) return true + if (value.length >= 16 && value.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' || it == '-' }) { + val digitCount = value.count(Char::isDigit) + val alphaCount = value.count { it.lowercaseChar() in 'a'..'f' } + if (digitCount >= 4 && alphaCount >= 4) return true + } + return false + } + + fun String.toReadableIdentityOrNull(): String? { + val value = trim() + if (value.isEmpty()) return null + if (value.isLikelyInternalId()) return null + if (!value.any { it.isLetter() }) return null + return value + } + + fun String.toSearchIdentityOrNull(): String? { + val value = trim() + if (value.isEmpty()) return null + if (value.equals("myai", ignoreCase = true)) return null + return value + } + + val exportTargets by rememberAsyncMutableState(defaultValue = emptyList()) { + context.messageLogger.getConversationExportTargets() + } + val exportSearchTargets by rememberAsyncMutableState( + defaultValue = emptyList(), + keys = arrayOf(exportTargets) + ) { + val friendIdentityCache = mutableMapOf?>() + exportTargets.mapIndexedNotNull { index, target -> + val friend = context.database.findFriend(target.conversationId) + val group = context.database.getGroupInfo(target.conversationId) + val chatDisplayName = target.groupTitle + ?.toReadableIdentityOrNull() + ?.takeIf { !it.equals(target.conversationId, ignoreCase = true) } + val friendDisplayName = friend?.displayName?.toReadableIdentityOrNull() + val friendUsername = friend?.mutableUsername?.toReadableIdentityOrNull() + val searchableUsernames = target.usernames + .mapNotNull { it.toSearchIdentityOrNull() } + .distinct() + val readableUsernames = searchableUsernames + .mapNotNull { it.toReadableIdentityOrNull() } + .distinct() + val hasManyParticipants = target.userIds.distinct().size > 2 || searchableUsernames.size > 2 + val fallbackFriendIdentities = if (friend == null && !hasManyParticipants) { + target.userIds.mapNotNull { userId -> + friendIdentityCache.getOrPut(userId) { + context.database.getFriendInfo(userId)?.let { + it.displayName?.toReadableIdentityOrNull() to + it.mutableUsername.toReadableIdentityOrNull() + } + }?.takeIf { it.first != null || it.second != null } + } + } else { + emptyList() + } + val fallbackFriendDisplayName = fallbackFriendIdentities.firstNotNullOfOrNull { it.first } + val fallbackFriendUsername = fallbackFriendIdentities.firstNotNullOfOrNull { it.second } + val resolvedFriendDisplayName = friendDisplayName ?: fallbackFriendDisplayName + val resolvedFriendUsername = friendUsername ?: fallbackFriendUsername + val groupDisplayName = group?.name?.toReadableIdentityOrNull() + ?: chatDisplayName?.takeIf { hasManyParticipants } + val isGroupChat = groupDisplayName != null || hasManyParticipants + val isDirectChat = !isGroupChat + val readableIdentifiers = buildList { + add(target.conversationId) + addAll(target.userIds) + resolvedFriendDisplayName?.let { add(it) } + resolvedFriendUsername?.let { add(it) } + chatDisplayName?.let { add(it) } + groupDisplayName?.let { add(it) } + addAll(searchableUsernames) + addAll(readableUsernames) + }.distinct() + ConversationSearchTarget( + target = target, + friendDisplayName = resolvedFriendDisplayName, + friendUsername = resolvedFriendUsername, + chatDisplayName = chatDisplayName, + groupDisplayName = groupDisplayName, + readableUsernames = readableUsernames, + readableIdentifiers = readableIdentifiers, + isDirectChat = isDirectChat, + isGroupChat = isGroupChat, + sortOrder = index + ) + }.sortedWith( + compareBy { + when { + it.isDirectChat -> 0 + it.isGroupChat -> 1 + else -> 2 + } + }.thenBy { it.sortOrder } + ) + } + val filteredExportTargets = remember(exportSearchTargets, conversationSearchQuery) { + val query = conversationSearchQuery.trim() + if (query.isBlank()) { + exportSearchTargets + } else { + exportSearchTargets.filter { searchTarget -> + searchTarget.readableIdentifiers.any { + it.contains(query, ignoreCase = true) + } + } + } + } + + val exportFormats = remember { + listOf( + ConversationExportFormat("db", "application/octet-stream", ".db"), + ConversationExportFormat("html", "text/html", "HTML"), + ConversationExportFormat("txt", "text/plain", "TXT") + ) + } + + fun formatExportTarget(searchTarget: ConversationSearchTarget): String { + searchTarget.friendDisplayName?.let { displayName -> + val username = searchTarget.friendUsername + val formattedName = if (username != null && !username.equals(displayName, ignoreCase = true)) { + "$displayName • @$username" + } else { + displayName + } + return loggerHistoryTranslation.format("list_friend_format", "name" to formattedName) + } + + searchTarget.friendUsername?.let { username -> + return loggerHistoryTranslation.format("list_friend_format", "name" to "@$username") + } + + searchTarget.chatDisplayName?.takeIf { searchTarget.isDirectChat }?.let { + return loggerHistoryTranslation.format("list_friend_format", "name" to it) + } + + if (searchTarget.isDirectChat && searchTarget.readableUsernames.isNotEmpty()) { + val friendName = if (searchTarget.readableUsernames.size == 1) { + searchTarget.readableUsernames.first() + } else { + searchTarget.readableUsernames.joinToString(", ") + } + return loggerHistoryTranslation.format("list_friend_format", "name" to friendName) + } + + searchTarget.groupDisplayName?.let { + return loggerHistoryTranslation.format("list_group_format", "name" to it) + } + + if (searchTarget.readableUsernames.isNotEmpty()) { + return loggerHistoryTranslation.format( + "list_group_format", + "name" to searchTarget.readableUsernames.joinToString(", ") + ) + } + + return if (searchTarget.isGroupChat) { + loggerHistoryTranslation.format("list_group_format", "name" to searchTarget.target.conversationId) + } else { + loggerHistoryTranslation.format("list_friend_format", "name" to searchTarget.target.conversationId) + } + } + + fun showExportError(throwable: Throwable) { + context.log.error("Failed to export message logger", throwable) + context.shortToast( + translation.format( + "message_logger_export_failed_toast", + "message" to (throwable.message ?: "Unknown error") + ) + ) + } + + fun showImportError(throwable: Throwable) { + context.log.error("Failed to import message logger", throwable) + context.shortToast( + translation.format( + "import_failed_toast", + "message" to (throwable.message ?: "Unknown error") + ) + ) + } + + fun parseConversationMessage(message: LoggedMessage): ParsedConversationMessage { + val messageObject = runCatching { + JsonParser.parseString(String(message.messageData, Charsets.UTF_8)).asJsonObject + }.getOrNull() + val messageContent = messageObject?.getAsJsonObject("mMessageContent") + val contentBytes = runCatching { + messageContent?.getAsJsonArray("mContent")?.map { it.asByte }?.toByteArray() + }.getOrNull() + val contentType = messageContent?.getAsJsonPrimitive("mContentType")?.asString?.let { + runCatching { ContentType.valueOf(it) }.getOrNull() + } ?: contentBytes?.let { ContentType.fromMessageContainer(ProtoReader(it)) } ?: ContentType.UNKNOWN + val messageText = contentBytes?.getMessageText(contentType) + val attachments = runCatching { + messageContent?.let { MessageDecoder.decode(it) } ?: emptyList() + }.getOrDefault(emptyList()) + + return ParsedConversationMessage( + senderId = message.userId, + senderUsername = message.username, + timestamp = message.sendTimestamp, + contentType = contentType, + messageText = messageText, + attachments = attachments + ) + } + + fun htmlEscape(input: String): String { + val escaped = StringBuilder(input.length) + input.forEach { char -> + when (char) { + '&' -> escaped.append("&") + '<' -> escaped.append("<") + '>' -> escaped.append(">") + '"' -> escaped.append(""") + '\'' -> escaped.append("'") + else -> escaped.append(char) + } + } + return escaped.toString() + } + + fun writeConversationExportFile( + target: LoggerConversationExportTarget, + format: ConversationExportFormat, + outputFile: File + ): Int { + val conversationId = target.conversationId.trim() + if (conversationId.isEmpty()) { + throw IllegalArgumentException("Conversation ID cannot be empty") + } + + val searchTarget = exportSearchTargets.firstOrNull { it.target.conversationId == conversationId } + val conversationTitle = searchTarget?.let { formatExportTarget(it) } + ?: (translation["message_logger_export_individual_chat"] ?: "Exported Chat") + val dateFormatter = DateFormat.getDateTimeInstance() + val senderCache = mutableMapOf() + + fun formatSenderLabel(senderId: String, senderUsername: String): String { + val friendInfo = context.database.getFriendInfo(senderId) + val senderDisplayName = friendInfo?.displayName?.toReadableIdentityOrNull() + val senderReadableUsername = friendInfo?.mutableUsername?.toReadableIdentityOrNull() + ?: senderUsername.toReadableIdentityOrNull() + return when { + senderDisplayName != null && + senderReadableUsername != null && + !senderDisplayName.equals(senderReadableUsername, ignoreCase = true) -> + "$senderDisplayName (@$senderReadableUsername)" + senderDisplayName != null -> senderDisplayName + senderReadableUsername != null -> "@$senderReadableUsername" + else -> translation["sender_unknown"] ?: "Unknown sender" + } + } + + outputFile.parentFile?.mkdirs() + if (outputFile.exists() && !outputFile.delete()) { + throw IllegalStateException("Failed to prepare export file") + } + + return outputFile.bufferedWriter(Charsets.UTF_8).use { writer -> + val isHtmlFormat = format.extension == "html" + if (isHtmlFormat) { + writer.appendLine("") + writer.appendLine("") + writer.appendLine("") + writer.appendLine("${htmlEscape(conversationTitle)}") + writer.appendLine( + "" + ) + writer.appendLine("") + writer.appendLine("

${htmlEscape(conversationTitle)}

") + writer.appendLine("

${htmlEscape(translation.format("message_logger_conversation_id", "id" to conversationId))}

") + } else { + writer.appendLine(conversationTitle) + writer.appendLine("") + } + + val exportedMessageCount = context.messageLogger.forEachConversationMessage( + conversationId = conversationId, + userIds = target.userIds, + orderAscending = true + ) { loggedMessage -> + val parsed = parseConversationMessage(loggedMessage) + val senderInfo = senderCache.getOrPut(parsed.senderId) { + formatSenderLabel(parsed.senderId, parsed.senderUsername) + } + val senderLabel = senderInfo + val content = parsed.messageText?.takeIf { it.isNotBlank() } ?: if (parsed.contentType == ContentType.CHAT) { + loggerHistoryTranslation["empty_message"] + } else { + parsed.contentType.name.lowercase() + } + + if (isHtmlFormat) { + writer.appendLine("
") + writer.appendLine( + "
${ + htmlEscape( + "${dateFormatter.format(Date(parsed.timestamp))} • $senderLabel • ${ + parsed.contentType.name.lowercase() + }" + ) + }
" + ) + writer.appendLine("
${htmlEscape(content).replace("\n", "
")}
") + if (parsed.attachments.isNotEmpty()) { + writer.appendLine("
    ") + parsed.attachments.forEachIndexed { index, attachment -> + val attachmentLabel = "${loggerHistoryTranslation.format("chat_attachment", "index" to (index + 1).toString())} [${attachment.type.name.lowercase()}]" + val directUrl = attachment.directUrl?.takeIf { it.isNotBlank() } + if (directUrl != null) { + writer.appendLine( + "
  • ${ + htmlEscape(attachmentLabel) + }
  • " + ) + } else { + val placeholder = attachment.boltKey?.takeIf { it.isNotBlank() } + ?: attachment.mediaUniqueId?.takeIf { it.isNotBlank() } + ?: (translation["message_logger_missing_attachment_placeholder"] ?: "Attachment unavailable") + writer.appendLine("
  • ${htmlEscape("$attachmentLabel: $placeholder")}
  • ") + } + } + writer.appendLine("
") + } + writer.appendLine("
") + } else { + writer.appendLine("[${dateFormatter.format(Date(parsed.timestamp))}] $senderLabel: $content") + parsed.attachments.forEachIndexed { index, attachment -> + val attachmentLabel = "${loggerHistoryTranslation.format("chat_attachment", "index" to (index + 1).toString())} [${attachment.type.name.lowercase()}]" + val attachmentValue = attachment.directUrl?.takeIf { it.isNotBlank() } + ?: (translation["message_logger_missing_attachment_placeholder"] ?: "Attachment unavailable") + writer.appendLine(" - $attachmentLabel: $attachmentValue") + } + writer.appendLine("") + } + } + + if (exportedMessageCount == 0) { + if (isHtmlFormat) { + writer.appendLine("

${htmlEscape(translation["message_logger_no_messages_export_text"] ?: "No messages found in this chat.")}

") + } else { + writer.appendLine(translation["message_logger_no_messages_export_text"] ?: "No messages found in this chat.") + } + } + + if (isHtmlFormat) { + writer.appendLine("") + } + + exportedMessageCount + } + } + + fun exportFullDatabase() { + runCatching { + activityLauncherHelper.saveFile("message_logger.db", "application/octet-stream") { uri -> + context.androidContext.contentResolver.openOutputStream(uri.toUri())?.use { out -> + context.messageLogger.databaseFile.inputStream().use { input -> input.copyTo(out) } + } ?: throw IllegalStateException("Failed to open output stream") + } + }.onFailure { showExportError(it) } + } + + fun exportConversation(target: LoggerConversationExportTarget, format: ConversationExportFormat) { + val conversationId = target.conversationId.trim() + if (conversationId.isEmpty()) { + context.shortToast(translation["message_logger_missing_conversation_toast"]) + return + } + + val fileNameSuffix = conversationId + .filter { it.isLetterOrDigit() || it == '-' || it == '_' } + .take(24) + .ifBlank { "chat" } + + runCatching { + activityLauncherHelper.saveFile("message_logger_${fileNameSuffix}.${format.extension}", format.mimeType) { uri -> + scope.launch { + runCatching { + val exportedMessageCount = withContext(Dispatchers.IO) { + val tempFile = File( + context.androidContext.cacheDir, + "message_logger_export_${System.currentTimeMillis()}.${format.extension}" + ) + try { + val messageCount = if (format.extension == "db") { + context.messageLogger.exportConversationDatabase( + outputFile = tempFile, + conversationId = conversationId, + userIds = target.userIds + ).messageCount + } else { + writeConversationExportFile(target, format, tempFile) + } + context.androidContext.contentResolver.openOutputStream(uri.toUri())?.use { output -> + tempFile.inputStream().use { input -> input.copyTo(output) } + } ?: throw IllegalStateException("Failed to open output stream") + messageCount + } finally { + tempFile.delete() + } + } + + if (exportedMessageCount == 0) { + context.shortToast(translation["message_logger_empty_chat_toast"]) + } else { + context.shortToast(translation["success_toast"]) + } + }.onFailure { showExportError(it) } + } + } + }.onFailure { showExportError(it) } + } + + fun dismissConversationExportDialog() { + showConversationExportDialog = false + selectedConversationForExport = null + conversationSearchQuery = "" + } + + fun dismissConversationFormatDialog() { + showConversationFormatDialog = false + pendingConversationExportTarget = null + } + Column(modifier = Modifier.fillMaxWidth().padding(5.dp), verticalArrangement = Arrangement.spacedBy(10.dp), horizontalAlignment = Alignment.CenterHorizontally) { val summary = translation.format("message_logger_summary", "messageCount" to storedMessagesCount.toString(), "storyCount" to storedStoriesCount.toString()).replace("\n", " | ") Text(summary, maxLines = 2, color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()) FlowRow(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), verticalArrangement = Arrangement.spacedBy(10.dp) ) { - Button(onClick = { runCatching { activityLauncherHelper.saveFile("message_logger.db", "application/octet-stream") { uri -> context.androidContext.contentResolver.openOutputStream(uri.toUri())?.use { out -> context.messageLogger.databaseFile.inputStream().use { it.copyTo(out) } } } }.onFailure { context.log.error("Failed to export", it) } }, colors = sharedButtonColors, border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))) { Text(text = translation["export_button"]) } + Button(onClick = { showExportOptionsDialog = true }, colors = sharedButtonColors, border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))) { Text(text = translation["export_button"]) } Button(onClick = { runCatching { activityLauncherHelper.openFile("application/octet-stream") { uri -> val tempFile = File(context.androidContext.cacheDir, "view_logger.db"); context.androidContext.contentResolver.openInputStream(uri.toUri())?.use { it.copyTo(tempFile.outputStream()) }; routes.viewLoggerHistory.navigate { put("uri", URLEncoder.encode(tempFile.toUri().toString(), "UTF-8")) } } } }, colors = sharedButtonColors, border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))) { Text(text = translation["view_button"]) } Button(onClick = { runCatching { context.messageLogger.purgeAll(); storedMessagesCount = 0; storedStoriesCount = 0 }.onSuccess { context.shortToast(translation["success_toast"]) } }, colors = sharedButtonColors, border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))) { Text(text = translation["clear_button"]) } Button(onClick = { showImportDialog = true }, colors = sharedButtonColors, border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))) { Text(text = translation["import_button"]) } @@ -269,7 +758,222 @@ fun HomeSettings.AphelionSettingsScreen(nav: NavBackStackEntry) { } OutlinedButton(modifier = Modifier.fillMaxWidth().padding(5.dp), onClick = { routes.loggerHistory.navigate() }, colors = sharedOutlinedColors, border = BorderStroke(1.dp, Color.White.copy(alpha = 0.2f))) { Text(translation["view_logger_history_button"]) } if (showImportDialog) { - AestheticDialog(onDismissRequest = { showImportDialog = false }, title = translation["message_logger_import_title"], text = translation["message_logger_import_text"], icon = Icons.Filled.Info, confirmButtonText = context.translation["button.import"], dismissButtonText = context.translation["button.cancel"], onConfirm = { showImportDialog = false; runCatching { activityLauncherHelper.openFile("application/octet-stream") { uri -> context.androidContext.contentResolver.openInputStream(uri.toUri())?.use { context.messageLogger.databaseFile.outputStream().use { out -> it.copyTo(out) } }; storedMessagesCount = context.messageLogger.getStoredMessageCount(); storedStoriesCount = context.messageLogger.getStoredStoriesCount(); context.shortToast(translation["success_toast"]) } } }, onDismiss = { showImportDialog = false }, showCloseButton = false) + AestheticDialog( + onDismissRequest = { showImportDialog = false }, + title = translation["message_logger_import_title"], + text = translation["message_logger_import_text"], + icon = Icons.Filled.Info, + confirmButtonText = context.translation["button.import"], + dismissButtonText = context.translation["button.cancel"], + onConfirm = { + showImportDialog = false + runCatching { + activityLauncherHelper.openFile("application/octet-stream") { uri -> + scope.launch { + runCatching { + val importResult = withContext(Dispatchers.IO) { + context.androidContext.contentResolver.openInputStream(uri.toUri())?.use { input -> + context.messageLogger.importDatabase(input) + } ?: throw IllegalStateException("Failed to open selected backup file") + } + storedMessagesCount = importResult.messageCount + storedStoriesCount = importResult.storyCount + context.shortToast(translation["success_toast"]) + }.onFailure { showImportError(it) } + } + } + }.onFailure { showImportError(it) } + }, + onDismiss = { showImportDialog = false }, + showCloseButton = false + ) + } + if (showExportOptionsDialog) { + AestheticDialog( + onDismissRequest = { showExportOptionsDialog = false }, + title = translation["message_logger_export_title"] ?: "Export Message Logger", + text = translation["message_logger_export_text"] ?: "Choose what to export.", + icon = Icons.Filled.SaveAlt, + confirmButtonText = context.translation["button.cancel"], + onConfirm = { showExportOptionsDialog = false }, + showCloseButton = false, + customContent = { + Button( + onClick = { + showExportOptionsDialog = false + pendingConversationExportTarget = null + showConversationExportDialog = true + }, + modifier = Modifier.fillMaxWidth(), + colors = sharedButtonColors, + border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)) + ) { + Text(translation["message_logger_export_individual_chat"] ?: "Export Individual Chat") + } + Button( + onClick = { + showExportOptionsDialog = false + exportFullDatabase() + }, + modifier = Modifier.fillMaxWidth(), + colors = sharedButtonColors, + border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)) + ) { + Text(translation["message_logger_export_full_database"] ?: "Export Full Database") + } + } + ) + } + if (showConversationExportDialog) { + AestheticDialog( + onDismissRequest = { dismissConversationExportDialog() }, + title = translation["message_logger_select_chat_title"] ?: "Export Individual Chat", + text = translation["message_logger_select_chat_text"] ?: "Search by username, display name, or chat name.", + icon = Icons.Filled.Search, + confirmButtonText = translation["message_logger_continue_button"] ?: "Continue", + dismissButtonText = context.translation["button.cancel"], + onConfirm = { + val selectedTarget = selectedConversationForExport ?: return@AestheticDialog + pendingConversationExportTarget = selectedTarget + dismissConversationExportDialog() + showConversationFormatDialog = true + }, + onDismiss = { dismissConversationExportDialog() }, + showCloseButton = false, + confirmEnabled = selectedConversationForExport != null, + customContent = { + OutlinedTextField( + value = conversationSearchQuery, + onValueChange = { conversationSearchQuery = it }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text(context.translation["manager.dialogs.add_friend.search_hint"] ?: "Search") + }, + leadingIcon = { + Icon(Icons.Filled.Search, contentDescription = null) + }, + trailingIcon = if (conversationSearchQuery.isNotBlank()) { + { + IconButton(onClick = { conversationSearchQuery = "" }) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = null + ) + } + } + } else null, + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedContainerColor = Color.White.copy(alpha = 0.08f), + unfocusedContainerColor = Color.White.copy(alpha = 0.06f), + focusedTextColor = Color.White, + unfocusedTextColor = Color.White + ) + ) + + if (filteredExportTargets.isEmpty()) { + Text( + text = translation["message_logger_no_chats_found"] ?: "No chats found", + color = PurrfectPalette.textSecondary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } else { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 280.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(filteredExportTargets.size) { index -> + val searchTarget = filteredExportTargets[index] + val target = searchTarget.target + val isSelected = selectedConversationForExport?.conversationId == searchTarget.target.conversationId + OutlinedButton( + onClick = { selectedConversationForExport = searchTarget.target }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White), + border = BorderStroke( + 1.dp, + if (isSelected) { + PurrfectPalette.glowPrimary.copy(alpha = 0.55f) + } else { + Color.White.copy(alpha = 0.18f) + } + ) + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = formatExportTarget(searchTarget), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + val secondaryLabel = when { + searchTarget.friendDisplayName != null && searchTarget.friendUsername != null -> "@${searchTarget.friendUsername}" + searchTarget.friendDisplayName != null -> searchTarget.friendDisplayName + searchTarget.chatDisplayName != null -> searchTarget.chatDisplayName + searchTarget.groupDisplayName != null -> searchTarget.groupDisplayName + searchTarget.readableUsernames.isNotEmpty() -> searchTarget.readableUsernames.joinToString(", ") + else -> null + } + if (secondaryLabel != null) { + Text( + text = secondaryLabel, + color = PurrfectPalette.textSecondary, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Text( + text = translation.format("message_logger_message_count", "count" to target.messageCount.toString()), + color = PurrfectPalette.textSecondary, + fontSize = 12.sp + ) + } + } + } + } + } + } + ) + } + if (showConversationFormatDialog && pendingConversationExportTarget != null) { + AestheticDialog( + onDismissRequest = { dismissConversationFormatDialog() }, + title = translation["message_logger_select_export_format_title"] ?: "Select Export Format", + text = translation["message_logger_select_export_format_text"] ?: "Choose how to export the selected chat.", + icon = Icons.Filled.Description, + confirmButtonText = context.translation["button.cancel"], + onConfirm = { dismissConversationFormatDialog() }, + showCloseButton = false, + customContent = { + exportFormats.forEach { format -> + val formatLabel = when (format.extension) { + "db" -> translation["message_logger_export_format_db"] ?: ".db" + "html" -> translation["message_logger_export_format_html"] ?: "HTML" + else -> translation["message_logger_export_format_txt"] ?: "TXT" + } + Button( + onClick = { + val exportTarget = pendingConversationExportTarget ?: return@Button + dismissConversationFormatDialog() + exportConversation(exportTarget, format) + }, + modifier = Modifier.fillMaxWidth(), + colors = sharedButtonColors, + border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)) + ) { + Text(formatLabel) + } + } + } + ) } } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionSocialView.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionSocialView.kt index 8bbb679e..f765b024 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionSocialView.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionSocialView.kt @@ -39,15 +39,20 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavBackStackEntry +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.eternal.purrfectsnap.R import me.eternal.purrfectsnap.common.data.SocialScope import me.eternal.purrfectsnap.ui.manager.pages.social.SocialRootSection +import me.eternal.purrfectsnap.ui.manager.pages.social.sortSocialFriends import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette @OptIn(ExperimentalFoundationApi::class) @Composable fun SocialRootSection.AphelionSocialScreen(nav: NavBackStackEntry) { + // Controller handles data loading and synchronization + SocialDataController() + val titles = remember { listOf(translation["friends_tab"], translation["groups_tab"]) } @@ -56,19 +61,9 @@ fun SocialRootSection.AphelionSocialScreen(nav: NavBackStackEntry) { var searchQuery by rememberSaveable { mutableStateOf("") } var searchActive by rememberSaveable { mutableStateOf(false) } - LaunchedEffect(Unit) { - context.database.receiveMessagingDataCallback = { friends, groups -> - friendList = friends - groupList = groups - } - updateScopeLists() - } - DisposableEffect(Unit) { - onDispose { - context.database.receiveMessagingDataCallback = { _, _ -> } - } - } val normalizedQuery = remember(searchQuery) { searchQuery.trim() } + + // Filter logic based on the parent's synchronized data lists val filteredFriends = remember(friendList, normalizedQuery) { if (normalizedQuery.isBlank()) { friendList diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/legacy/LegacyTheme.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/legacy/LegacyTheme.kt index dff2aca6..f6628658 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/legacy/LegacyTheme.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/legacy/LegacyTheme.kt @@ -3,6 +3,7 @@ package me.eternal.purrfectsnap.ui.manager.pages.themes.legacy import android.os.SystemClock import android.content.SharedPreferences import android.content.Intent +import com.google.gson.JsonParser import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi @@ -77,21 +78,33 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.eternal.purrfectsnap.LogLine +import me.eternal.purrfectsnap.ui.manager.ManagerAssistantEntry +import me.eternal.purrfectsnap.ui.manager.ManagerAssistantTriggerStyle import me.eternal.purrfectsnap.LogReader import me.eternal.purrfectsnap.R import me.eternal.purrfectsnap.action.EnumQuickActions import me.eternal.purrfectsnap.common.BuildConfig import me.eternal.purrfectsnap.common.action.EnumAction import me.eternal.purrfectsnap.common.bridge.InternalFileHandleType +import me.eternal.purrfectsnap.common.bridge.wrapper.LoggerConversationExportTarget +import me.eternal.purrfectsnap.common.bridge.wrapper.LoggedMessage import me.eternal.purrfectsnap.common.config.ConfigContainer import me.eternal.purrfectsnap.common.config.PropertyPair +import me.eternal.purrfectsnap.common.data.ContentType import me.eternal.purrfectsnap.common.data.SocialScope import me.eternal.purrfectsnap.common.ui.TopBarActionButton import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableStateList import me.eternal.purrfectsnap.common.util.ktx.copyToClipboard import me.eternal.purrfectsnap.common.util.ktx.openLink +import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader +import me.eternal.purrfectsnap.core.features.impl.downloader.decoder.DecodedAttachment +import me.eternal.purrfectsnap.core.features.impl.downloader.decoder.MessageDecoder +import me.eternal.purrfectsnap.core.wrapper.impl.getMessageText +import me.eternal.purrfectsnap.storage.findFriend import me.eternal.purrfectsnap.storage.getAllScopeNotes +import me.eternal.purrfectsnap.storage.getFriendInfo +import me.eternal.purrfectsnap.storage.getGroupInfo import me.eternal.purrfectsnap.storage.getQuickTiles import me.eternal.purrfectsnap.storage.setAllScopeNotes import me.eternal.purrfectsnap.storage.setQuickTiles @@ -113,6 +126,7 @@ import me.eternal.purrfectsnap.ui.manager.pages.home.HomeSettings import me.eternal.purrfectsnap.ui.manager.pages.home.QuickActionsDialog import me.eternal.purrfectsnap.ui.manager.pages.scripting.ScriptingRootSection import me.eternal.purrfectsnap.ui.manager.pages.social.SocialRootSection +import me.eternal.purrfectsnap.ui.manager.pages.social.sortSocialFriends import me.eternal.purrfectsnap.ui.manager.pages.tracker.FriendTrackerManagerRoot import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette import me.eternal.purrfectsnap.ui.setup.Requirements @@ -130,6 +144,8 @@ import okhttp3.Request import java.io.File import java.io.FileOutputStream import java.net.URLEncoder +import java.text.DateFormat +import java.util.Date object LegacyTheme : ThemeContract { @OptIn(ExperimentalLayoutApi::class, ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) @@ -140,24 +156,28 @@ object LegacyTheme : ThemeContract { icon: ImageVector, label: String? = null, contentDescription: String? = label, + modifier: Modifier = Modifier, onClick: () -> Unit, ) { Surface( + modifier = modifier.height(36.dp), shape = RoundedCornerShape(40), color = Color.White.copy(alpha = 0.06f), border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)) ) { Row( modifier = Modifier + .fillMaxWidth() .clip(RoundedCornerShape(40)) .clickable(onClick = onClick) - .padding(horizontal = 14.dp, vertical = 8.dp), + .padding(horizontal = 10.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.Center ) { - Icon(icon, contentDescription = contentDescription, tint = Color.White) + Icon(icon, contentDescription = contentDescription, tint = Color.White, modifier = Modifier.size(20.dp)) label?.let { - Text(text = it, color = Color.White, fontSize = 13.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis) + Spacer(modifier = Modifier.width(6.dp)) + Text(text = it, color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis) } } } @@ -165,8 +185,22 @@ object LegacyTheme : ThemeContract { @Composable fun RowScope.LocalHomeActionChips() { - LocalTopBarActionChip(icon = Icons.Filled.BugReport, label = context.translation["manager.routes.home_logs"]) { routes.homeLogs.navigate() } - LocalTopBarActionChip(icon = Icons.Filled.Info, label = translation["manager.routes.home_about"]) { routes.about.navigate() } + ManagerAssistantEntry( + context = context, + routes = routes, + style = ManagerAssistantTriggerStyle.DEFAULT, + modifier = Modifier.weight(1f) + ) + LocalTopBarActionChip( + icon = Icons.Filled.BugReport, + label = context.translation["manager.routes.home_logs"], + modifier = Modifier.weight(1f) + ) { routes.homeLogs.navigate() } + LocalTopBarActionChip( + icon = Icons.Filled.Info, + label = translation["manager.routes.home_about"], + modifier = Modifier.weight(1f) + ) { routes.about.navigate() } } @Composable @@ -176,7 +210,6 @@ object LegacyTheme : ThemeContract { downloadState: UpdateDownloader.DownloadState, downloadProgress: Float, onUpdateAction: () -> Unit, - channelLabel: String, isPurrAuraActive: Boolean, onWebsiteClick: () -> Unit, onTelegramClick: () -> Unit, @@ -209,7 +242,7 @@ object LegacyTheme : ThemeContract { horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), verticalArrangement = Arrangement.spacedBy(10.dp) ) { - HeroBadge(translation.format("hero_version_label", "version" to versionName, "channel" to channelLabel)) + HeroBadge(translation.format("hero_version_label", "version" to versionName)) gitHashShort.takeIf { it.isNotBlank() && it.lowercase() != "unknown" }?.let { HeroBadge(translation.format("hero_build_label", "build" to it)) } @@ -323,19 +356,18 @@ object LegacyTheme : ThemeContract { } hasInitializedQuickTiles -> storedTiles else -> { - context.database.setQuickTiles(allQuickTileNames) - prefs.edit().putBoolean(QUICK_TILES_INITIALIZED_PREF, true).apply() + context.coroutineScope.launch(Dispatchers.IO) { + context.database.setQuickTiles(allQuickTileNames) + prefs.edit().putBoolean(QUICK_TILES_INITIALIZED_PREF, true).apply() + } allQuickTileNames } } } - val updateChannel = context.config.root.global.updateSettings.updateChannel.getNullable() ?: "stable" - val channelLabel = if (updateChannel == "prerelease") translation["channel_label_prerelease"] ?: "" else translation["channel_label_stable"] ?: "" - val latestUpdate by rememberAsyncMutableState(defaultValue = null, keys = arrayOf(updateChannel)) { - val channel = if (updateChannel == "prerelease") Channel.PRERELEASE else Channel.STABLE - Updater.getLatestRelease(channel) + val latestUpdate by rememberAsyncMutableState(defaultValue = null) { + Updater.getLatestRelease(Channel.STABLE) } - val changelogUrl = if (updateChannel == "prerelease") changelogPrereleaseUrl else changelogStableUrl + val changelogUrl = changelogStableUrl val downloadState by UpdateDownloader.downloadState.collectAsState() val downloadProgress by UpdateDownloader.downloadProgress.collectAsState() val coroutineScope = rememberCoroutineScope() @@ -464,18 +496,26 @@ object LegacyTheme : ThemeContract { Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(bottom = contentBottomPadding)) { Row( modifier = Modifier.fillMaxWidth().padding(WindowInsets.statusBars.asPaddingValues()).padding(horizontal = cardMargin, vertical = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { - Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) { - LocalTopBarActionChip(icon = Icons.Filled.Notifications, label = null, contentDescription = translation["announcements_button_description"]) { - showAnnouncementsDialog = true; loadAnnouncements() - } - LocalTopBarActionChip(icon = Icons.Filled.Description, label = null, contentDescription = translation.getOrNull("changelog_button_description") ?: "Open full changelog") { - showFullChangelogDialog = true; loadFullChangelog(changelogUrl) - } - } - Row(modifier = Modifier.wrapContentWidth(Alignment.End), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) { + LocalTopBarActionChip( + icon = Icons.Filled.Notifications, + label = null, + modifier = Modifier.width(56.dp), + contentDescription = translation["announcements_button_description"] + ) { showAnnouncementsDialog = true; loadAnnouncements() } + LocalTopBarActionChip( + icon = Icons.Filled.Description, + label = null, + modifier = Modifier.width(56.dp), + contentDescription = translation.getOrNull("changelog_button_description") ?: "Open full changelog" + ) { showFullChangelogDialog = true; loadFullChangelog(changelogUrl) } + Row( + modifier = Modifier.weight(1f), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { LocalHomeActionChips() } } @@ -486,7 +526,6 @@ object LegacyTheme : ThemeContract { downloadState = downloadState, downloadProgress = downloadProgress, onUpdateAction = onUpdateButtonClick, - channelLabel = channelLabel, isPurrAuraActive = isPurrAuraActive, onWebsiteClick = { context.androidContext.openLink("https://purrfectsnap.vercel.app/", context.translation["toast_open_link_failed"]) }, onTelegramClick = { context.androidContext.openLink("https://t.me/purrfectsnap_official", context.translation["toast_open_link_failed"]) }, @@ -831,24 +870,12 @@ object LegacyTheme : ThemeContract { RowTitle(title = translation["updates_title"]) Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { var autoUpdateCheck by remember { mutableStateOf(context.config.root.global.updateSettings.autoUpdateCheck.getNullable() ?: true) } - var selectedChannel by remember { mutableStateOf(context.config.root.global.updateSettings.updateChannel.getNullable() ?: "stable") } - var channelMenuExpanded by remember { mutableStateOf(false) } ShiftedRow { Row(modifier = Modifier.fillMaxWidth().heightIn(min = 55.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { Text(text = translation["auto_update_check"], fontSize = 14.sp) Switch(checked = autoUpdateCheck, onCheckedChange = { if (context.config.root.global.uiSettings.hapticFeedback.get()) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress); autoUpdateCheck = it; context.config.root.global.updateSettings.autoUpdateCheck.set(it); context.config.writeConfig(); scheduleUpdateCheck() }, modifier = Modifier.padding(end = 26.dp), colors = purrfectSwitchColors()) } } - AnimatedVisibility(visible = autoUpdateCheck) { - ExposedDropdownMenuBox(expanded = channelMenuExpanded, onExpandedChange = { channelMenuExpanded = it }, modifier = Modifier.fillMaxWidth().padding(horizontal = 26.dp)) { - AestheticDropdownField(value = translation.getOrNull("update_channel_${selectedChannel}") ?: selectedChannel, expanded = channelMenuExpanded, modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable), onClick = { channelMenuExpanded = true }) - ExposedDropdownMenu(expanded = channelMenuExpanded, onDismissRequest = { channelMenuExpanded = false }) { - listOf("stable", "prerelease").forEach { channel -> - DropdownMenuItem(text = { Text(text = translation.getOrNull("update_channel_${channel}") ?: channel) }, onClick = { selectedChannel = channel; channelMenuExpanded = false; context.config.root.global.updateSettings.updateChannel.set(channel); context.config.writeConfig(); scheduleUpdateCheck() }) - } - } - } - } } } @@ -866,11 +893,494 @@ object LegacyTheme : ThemeContract { var storedMessagesCount by rememberAsyncMutableState(defaultValue = 0) { context.messageLogger.getStoredMessageCount() } var storedStoriesCount by rememberAsyncMutableState(defaultValue = 0) { context.messageLogger.getStoredStoriesCount() } var showImportDialog by remember { mutableStateOf(false) } + var showExportOptionsDialog by remember { mutableStateOf(false) } + var showConversationExportDialog by remember { mutableStateOf(false) } + var showConversationFormatDialog by remember { mutableStateOf(false) } + var conversationSearchQuery by remember { mutableStateOf("") } + var selectedConversationForExport by remember { mutableStateOf(null) } + var pendingConversationExportTarget by remember { mutableStateOf(null) } + val loggerHistoryTranslation = remember { context.translation.getCategory("logger_history") } + + data class ConversationSearchTarget( + val target: LoggerConversationExportTarget, + val friendDisplayName: String?, + val friendUsername: String?, + val chatDisplayName: String?, + val groupDisplayName: String?, + val readableUsernames: List, + val readableIdentifiers: List, + val isDirectChat: Boolean, + val isGroupChat: Boolean, + val sortOrder: Int + ) + + data class ConversationExportFormat( + val extension: String, + val mimeType: String, + val label: String + ) + + data class ParsedConversationMessage( + val senderId: String, + val senderUsername: String, + val timestamp: Long, + val contentType: ContentType, + val messageText: String?, + val attachments: List + ) + + fun String.isUuidLike(): Boolean { + val value = trim() + if (value.length != 36) return false + if (value[8] != '-' || value[13] != '-' || value[18] != '-' || value[23] != '-') return false + return value.filterIndexed { index, _ -> + index != 8 && index != 13 && index != 18 && index != 23 + }.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' } + } + + fun String.isLikelyInternalId(): Boolean { + val value = trim() + if (value.isUuidLike()) return true + if (value.length >= 10 && value.all(Char::isDigit)) return true + if (value.length >= 16 && value.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' || it == '-' }) { + val digitCount = value.count(Char::isDigit) + val alphaCount = value.count { it.lowercaseChar() in 'a'..'f' } + if (digitCount >= 4 && alphaCount >= 4) return true + } + return false + } + + fun String.toReadableIdentityOrNull(): String? { + val value = trim() + if (value.isEmpty()) return null + if (value.isLikelyInternalId()) return null + if (!value.any { it.isLetter() }) return null + return value + } + + fun String.toSearchIdentityOrNull(): String? { + val value = trim() + if (value.isEmpty()) return null + if (value.equals("myai", ignoreCase = true)) return null + return value + } + + val exportTargets by rememberAsyncMutableState(defaultValue = emptyList()) { + context.messageLogger.getConversationExportTargets() + } + val exportSearchTargets by rememberAsyncMutableState( + defaultValue = emptyList(), + keys = arrayOf(exportTargets) + ) { + val friendIdentityCache = mutableMapOf?>() + exportTargets.mapIndexedNotNull { index, target -> + val friend = context.database.findFriend(target.conversationId) + val group = context.database.getGroupInfo(target.conversationId) + val chatDisplayName = target.groupTitle + ?.toReadableIdentityOrNull() + ?.takeIf { !it.equals(target.conversationId, ignoreCase = true) } + val friendDisplayName = friend?.displayName?.toReadableIdentityOrNull() + val friendUsername = friend?.mutableUsername?.toReadableIdentityOrNull() + val searchableUsernames = target.usernames + .mapNotNull { it.toSearchIdentityOrNull() } + .distinct() + val readableUsernames = searchableUsernames + .mapNotNull { it.toReadableIdentityOrNull() } + .distinct() + val hasManyParticipants = target.userIds.distinct().size > 2 || searchableUsernames.size > 2 + val fallbackFriendIdentities = if (friend == null && !hasManyParticipants) { + target.userIds.mapNotNull { userId -> + friendIdentityCache.getOrPut(userId) { + context.database.getFriendInfo(userId)?.let { + it.displayName?.toReadableIdentityOrNull() to + it.mutableUsername.toReadableIdentityOrNull() + } + }?.takeIf { it.first != null || it.second != null } + } + } else { + emptyList() + } + val fallbackFriendDisplayName = fallbackFriendIdentities.firstNotNullOfOrNull { it.first } + val fallbackFriendUsername = fallbackFriendIdentities.firstNotNullOfOrNull { it.second } + val resolvedFriendDisplayName = friendDisplayName ?: fallbackFriendDisplayName + val resolvedFriendUsername = friendUsername ?: fallbackFriendUsername + val groupDisplayName = group?.name?.toReadableIdentityOrNull() + ?: chatDisplayName?.takeIf { hasManyParticipants } + val isGroupChat = groupDisplayName != null || hasManyParticipants + val isDirectChat = !isGroupChat + val readableIdentifiers = buildList { + add(target.conversationId) + addAll(target.userIds) + resolvedFriendDisplayName?.let { add(it) } + resolvedFriendUsername?.let { add(it) } + chatDisplayName?.let { add(it) } + groupDisplayName?.let { add(it) } + addAll(searchableUsernames) + addAll(readableUsernames) + }.distinct() + ConversationSearchTarget( + target = target, + friendDisplayName = resolvedFriendDisplayName, + friendUsername = resolvedFriendUsername, + chatDisplayName = chatDisplayName, + groupDisplayName = groupDisplayName, + readableUsernames = readableUsernames, + readableIdentifiers = readableIdentifiers, + isDirectChat = isDirectChat, + isGroupChat = isGroupChat, + sortOrder = index + ) + }.sortedWith( + compareBy { + when { + it.isDirectChat -> 0 + it.isGroupChat -> 1 + else -> 2 + } + }.thenBy { it.sortOrder } + ) + } + val filteredExportTargets = remember(exportSearchTargets, conversationSearchQuery) { + val query = conversationSearchQuery.trim() + if (query.isBlank()) { + exportSearchTargets + } else { + exportSearchTargets.filter { searchTarget -> + searchTarget.readableIdentifiers.any { + it.contains(query, ignoreCase = true) + } + } + } + } + + val exportFormats = remember { + listOf( + ConversationExportFormat("db", "application/octet-stream", ".db"), + ConversationExportFormat("html", "text/html", "HTML"), + ConversationExportFormat("txt", "text/plain", "TXT") + ) + } + + fun formatExportTarget(searchTarget: ConversationSearchTarget): String { + searchTarget.friendDisplayName?.let { displayName -> + val username = searchTarget.friendUsername + val formattedName = if (username != null && !username.equals(displayName, ignoreCase = true)) { + "$displayName • @$username" + } else { + displayName + } + return loggerHistoryTranslation.format("list_friend_format", "name" to formattedName) + } + + searchTarget.friendUsername?.let { username -> + return loggerHistoryTranslation.format("list_friend_format", "name" to "@$username") + } + + searchTarget.chatDisplayName?.takeIf { searchTarget.isDirectChat }?.let { + return loggerHistoryTranslation.format("list_friend_format", "name" to it) + } + + if (searchTarget.isDirectChat && searchTarget.readableUsernames.isNotEmpty()) { + val friendName = if (searchTarget.readableUsernames.size == 1) { + searchTarget.readableUsernames.first() + } else { + searchTarget.readableUsernames.joinToString(", ") + } + return loggerHistoryTranslation.format("list_friend_format", "name" to friendName) + } + + searchTarget.groupDisplayName?.let { + return loggerHistoryTranslation.format("list_group_format", "name" to it) + } + + if (searchTarget.readableUsernames.isNotEmpty()) { + return loggerHistoryTranslation.format( + "list_group_format", + "name" to searchTarget.readableUsernames.joinToString(", ") + ) + } + + return if (searchTarget.isGroupChat) { + loggerHistoryTranslation.format("list_group_format", "name" to searchTarget.target.conversationId) + } else { + loggerHistoryTranslation.format("list_friend_format", "name" to searchTarget.target.conversationId) + } + } + + fun showExportError(throwable: Throwable) { + context.log.error("Failed to export message logger", throwable) + context.shortToast( + translation.format( + "message_logger_export_failed_toast", + "message" to (throwable.message ?: "Unknown error") + ) + ) + } + + fun showImportError(throwable: Throwable) { + context.log.error("Failed to import message logger", throwable) + context.shortToast( + translation.format( + "import_failed_toast", + "message" to (throwable.message ?: "Unknown error") + ) + ) + } + + fun parseConversationMessage(message: LoggedMessage): ParsedConversationMessage { + val messageObject = runCatching { + JsonParser.parseString(String(message.messageData, Charsets.UTF_8)).asJsonObject + }.getOrNull() + val messageContent = messageObject?.getAsJsonObject("mMessageContent") + val contentBytes = runCatching { + messageContent?.getAsJsonArray("mContent")?.map { it.asByte }?.toByteArray() + }.getOrNull() + val contentType = messageContent?.getAsJsonPrimitive("mContentType")?.asString?.let { + runCatching { ContentType.valueOf(it) }.getOrNull() + } ?: contentBytes?.let { ContentType.fromMessageContainer(ProtoReader(it)) } ?: ContentType.UNKNOWN + val messageText = contentBytes?.getMessageText(contentType) + val attachments = runCatching { + messageContent?.let { MessageDecoder.decode(it) } ?: emptyList() + }.getOrDefault(emptyList()) + + return ParsedConversationMessage( + senderId = message.userId, + senderUsername = message.username, + timestamp = message.sendTimestamp, + contentType = contentType, + messageText = messageText, + attachments = attachments + ) + } + + fun htmlEscape(input: String): String { + val escaped = StringBuilder(input.length) + input.forEach { char -> + when (char) { + '&' -> escaped.append("&") + '<' -> escaped.append("<") + '>' -> escaped.append(">") + '"' -> escaped.append(""") + '\'' -> escaped.append("'") + else -> escaped.append(char) + } + } + return escaped.toString() + } + + fun writeConversationExportFile( + target: LoggerConversationExportTarget, + format: ConversationExportFormat, + outputFile: File + ): Int { + val conversationId = target.conversationId.trim() + if (conversationId.isEmpty()) { + throw IllegalArgumentException("Conversation ID cannot be empty") + } + + val searchTarget = exportSearchTargets.firstOrNull { it.target.conversationId == conversationId } + val conversationTitle = searchTarget?.let { formatExportTarget(it) } + ?: (translation["message_logger_export_individual_chat"] ?: "Exported Chat") + val dateFormatter = DateFormat.getDateTimeInstance() + val senderCache = mutableMapOf() + + fun formatSenderLabel(senderId: String, senderUsername: String): String { + val friendInfo = context.database.getFriendInfo(senderId) + val senderDisplayName = friendInfo?.displayName?.toReadableIdentityOrNull() + val senderReadableUsername = friendInfo?.mutableUsername?.toReadableIdentityOrNull() + ?: senderUsername.toReadableIdentityOrNull() + return when { + senderDisplayName != null && + senderReadableUsername != null && + !senderDisplayName.equals(senderReadableUsername, ignoreCase = true) -> + "$senderDisplayName (@$senderReadableUsername)" + senderDisplayName != null -> senderDisplayName + senderReadableUsername != null -> "@$senderReadableUsername" + else -> translation["sender_unknown"] ?: "Unknown sender" + } + } + + outputFile.parentFile?.mkdirs() + if (outputFile.exists() && !outputFile.delete()) { + throw IllegalStateException("Failed to prepare export file") + } + + return outputFile.bufferedWriter(Charsets.UTF_8).use { writer -> + val isHtmlFormat = format.extension == "html" + if (isHtmlFormat) { + writer.appendLine("") + writer.appendLine("") + writer.appendLine("") + writer.appendLine("${htmlEscape(conversationTitle)}") + writer.appendLine( + "" + ) + writer.appendLine("") + writer.appendLine("

${htmlEscape(conversationTitle)}

") + writer.appendLine("

${htmlEscape(translation.format("message_logger_conversation_id", "id" to conversationId))}

") + } else { + writer.appendLine(conversationTitle) + writer.appendLine("") + } + + val exportedMessageCount = context.messageLogger.forEachConversationMessage( + conversationId = conversationId, + userIds = target.userIds, + orderAscending = true + ) { loggedMessage -> + val parsed = parseConversationMessage(loggedMessage) + val senderInfo = senderCache.getOrPut(parsed.senderId) { + formatSenderLabel(parsed.senderId, parsed.senderUsername) + } + val senderLabel = senderInfo + val content = parsed.messageText?.takeIf { it.isNotBlank() } ?: if (parsed.contentType == ContentType.CHAT) { + loggerHistoryTranslation["empty_message"] + } else { + parsed.contentType.name.lowercase() + } + + if (isHtmlFormat) { + writer.appendLine("
") + writer.appendLine( + "
${ + htmlEscape( + "${dateFormatter.format(Date(parsed.timestamp))} • $senderLabel • ${ + parsed.contentType.name.lowercase() + }" + ) + }
" + ) + writer.appendLine("
${htmlEscape(content).replace("\n", "
")}
") + if (parsed.attachments.isNotEmpty()) { + writer.appendLine("
    ") + parsed.attachments.forEachIndexed { index, attachment -> + val attachmentLabel = "${loggerHistoryTranslation.format("chat_attachment", "index" to (index + 1).toString())} [${attachment.type.name.lowercase()}]" + val directUrl = attachment.directUrl?.takeIf { it.isNotBlank() } + if (directUrl != null) { + writer.appendLine( + "
  • ${ + htmlEscape(attachmentLabel) + }
  • " + ) + } else { + val placeholder = attachment.boltKey?.takeIf { it.isNotBlank() } + ?: attachment.mediaUniqueId?.takeIf { it.isNotBlank() } + ?: (translation["message_logger_missing_attachment_placeholder"] ?: "Attachment unavailable") + writer.appendLine("
  • ${htmlEscape("$attachmentLabel: $placeholder")}
  • ") + } + } + writer.appendLine("
") + } + writer.appendLine("
") + } else { + writer.appendLine("[${dateFormatter.format(Date(parsed.timestamp))}] $senderLabel: $content") + parsed.attachments.forEachIndexed { index, attachment -> + val attachmentLabel = "${loggerHistoryTranslation.format("chat_attachment", "index" to (index + 1).toString())} [${attachment.type.name.lowercase()}]" + val attachmentValue = attachment.directUrl?.takeIf { it.isNotBlank() } + ?: (translation["message_logger_missing_attachment_placeholder"] ?: "Attachment unavailable") + writer.appendLine(" - $attachmentLabel: $attachmentValue") + } + writer.appendLine("") + } + } + + if (exportedMessageCount == 0) { + if (isHtmlFormat) { + writer.appendLine("

${htmlEscape(translation["message_logger_no_messages_export_text"] ?: "No messages found in this chat.")}

") + } else { + writer.appendLine(translation["message_logger_no_messages_export_text"] ?: "No messages found in this chat.") + } + } + + if (isHtmlFormat) { + writer.appendLine("") + } + + exportedMessageCount + } + } + + fun exportFullDatabase() { + runCatching { + activityLauncherHelper.saveFile("message_logger.db", "application/octet-stream") { uri -> + context.androidContext.contentResolver.openOutputStream(uri.toUri())?.use { out -> + context.messageLogger.databaseFile.inputStream().use { input -> input.copyTo(out) } + } ?: throw IllegalStateException("Failed to open output stream") + } + }.onFailure { showExportError(it) } + } + + fun exportConversation(target: LoggerConversationExportTarget, format: ConversationExportFormat) { + val conversationId = target.conversationId.trim() + if (conversationId.isEmpty()) { + context.shortToast(translation["message_logger_missing_conversation_toast"]) + return + } + + val fileNameSuffix = conversationId + .filter { it.isLetterOrDigit() || it == '-' || it == '_' } + .take(24) + .ifBlank { "chat" } + + runCatching { + activityLauncherHelper.saveFile("message_logger_${fileNameSuffix}.${format.extension}", format.mimeType) { uri -> + scope.launch { + runCatching { + val exportedMessageCount = withContext(Dispatchers.IO) { + val tempFile = File( + context.androidContext.cacheDir, + "message_logger_export_${System.currentTimeMillis()}.${format.extension}" + ) + try { + val messageCount = if (format.extension == "db") { + context.messageLogger.exportConversationDatabase( + outputFile = tempFile, + conversationId = conversationId, + userIds = target.userIds + ).messageCount + } else { + writeConversationExportFile(target, format, tempFile) + } + context.androidContext.contentResolver.openOutputStream(uri.toUri())?.use { output -> + tempFile.inputStream().use { input -> input.copyTo(output) } + } ?: throw IllegalStateException("Failed to open output stream") + messageCount + } finally { + tempFile.delete() + } + } + + if (exportedMessageCount == 0) { + context.shortToast(translation["message_logger_empty_chat_toast"]) + } else { + context.shortToast(translation["success_toast"]) + } + }.onFailure { showExportError(it) } + } + } + }.onFailure { showExportError(it) } + } + + fun dismissConversationExportDialog() { + showConversationExportDialog = false + selectedConversationForExport = null + conversationSearchQuery = "" + } + + fun dismissConversationFormatDialog() { + showConversationFormatDialog = false + pendingConversationExportTarget = null + } + Column(modifier = Modifier.fillMaxWidth().padding(5.dp), verticalArrangement = Arrangement.spacedBy(10.dp), horizontalAlignment = Alignment.CenterHorizontally) { val summary = translation.format("message_logger_summary", "messageCount" to storedMessagesCount.toString(), "storyCount" to storedStoriesCount.toString()).replace("\n", " | ") Text(summary, maxLines = 2, color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()) FlowRow(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), verticalArrangement = Arrangement.spacedBy(10.dp) ) { - Button(onClick = { runCatching { activityLauncherHelper.saveFile("message_logger.db", "application/octet-stream") { uri -> context.androidContext.contentResolver.openOutputStream(uri.toUri())?.use { out -> context.messageLogger.databaseFile.inputStream().use { it.copyTo(out) } } } }.onFailure { context.log.error("Failed to export", it) } }, colors = sharedButtonColors, border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))) { Text(text = translation["export_button"]) } + Button(onClick = { showExportOptionsDialog = true }, colors = sharedButtonColors, border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))) { Text(text = translation["export_button"]) } Button(onClick = { runCatching { activityLauncherHelper.openFile("application/octet-stream") { uri -> val tempFile = File(context.androidContext.cacheDir, "view_logger.db"); context.androidContext.contentResolver.openInputStream(uri.toUri())?.use { it.copyTo(tempFile.outputStream()) }; routes.viewLoggerHistory.navigate { put("uri", URLEncoder.encode(tempFile.toUri().toString(), "UTF-8")) } } } }, colors = sharedButtonColors, border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))) { Text(text = translation["view_button"]) } Button(onClick = { runCatching { context.messageLogger.purgeAll(); storedMessagesCount = 0; storedStoriesCount = 0 }.onSuccess { context.shortToast(translation["success_toast"]) } }, colors = sharedButtonColors, border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))) { Text(text = translation["clear_button"]) } Button(onClick = { showImportDialog = true }, colors = sharedButtonColors, border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))) { Text(text = translation["import_button"]) } @@ -878,7 +1388,222 @@ object LegacyTheme : ThemeContract { } OutlinedButton(modifier = Modifier.fillMaxWidth().padding(5.dp), onClick = { routes.loggerHistory.navigate() }, colors = sharedOutlinedColors, border = BorderStroke(1.dp, Color.White.copy(alpha = 0.2f))) { Text(translation["view_logger_history_button"]) } if (showImportDialog) { - AestheticDialog(onDismissRequest = { showImportDialog = false }, title = translation["message_logger_import_title"], text = translation["message_logger_import_text"], icon = Icons.Filled.Info, confirmButtonText = importLabel, dismissButtonText = context.translation["button.cancel"], onConfirm = { showImportDialog = false; runCatching { activityLauncherHelper.openFile("application/octet-stream") { uri -> context.androidContext.contentResolver.openInputStream(uri.toUri())?.use { context.messageLogger.databaseFile.outputStream().use { out -> it.copyTo(out) } }; storedMessagesCount = context.messageLogger.getStoredMessageCount(); storedStoriesCount = context.messageLogger.getStoredStoriesCount(); context.shortToast(translation["success_toast"]) } } }, onDismiss = { showImportDialog = false }, showCloseButton = false) + AestheticDialog( + onDismissRequest = { showImportDialog = false }, + title = translation["message_logger_import_title"], + text = translation["message_logger_import_text"], + icon = Icons.Filled.Info, + confirmButtonText = importLabel, + dismissButtonText = context.translation["button.cancel"], + onConfirm = { + showImportDialog = false + runCatching { + activityLauncherHelper.openFile("application/octet-stream") { uri -> + scope.launch { + runCatching { + val importResult = withContext(Dispatchers.IO) { + context.androidContext.contentResolver.openInputStream(uri.toUri())?.use { input -> + context.messageLogger.importDatabase(input) + } ?: throw IllegalStateException("Failed to open selected backup file") + } + storedMessagesCount = importResult.messageCount + storedStoriesCount = importResult.storyCount + context.shortToast(translation["success_toast"]) + }.onFailure { showImportError(it) } + } + } + }.onFailure { showImportError(it) } + }, + onDismiss = { showImportDialog = false }, + showCloseButton = false + ) + } + if (showExportOptionsDialog) { + AestheticDialog( + onDismissRequest = { showExportOptionsDialog = false }, + title = translation["message_logger_export_title"] ?: "Export Message Logger", + text = translation["message_logger_export_text"] ?: "Choose what to export.", + icon = Icons.Filled.SaveAlt, + confirmButtonText = context.translation["button.cancel"], + onConfirm = { showExportOptionsDialog = false }, + showCloseButton = false, + customContent = { + Button( + onClick = { + showExportOptionsDialog = false + pendingConversationExportTarget = null + showConversationExportDialog = true + }, + modifier = Modifier.fillMaxWidth(), + colors = sharedButtonColors, + border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)) + ) { + Text(translation["message_logger_export_individual_chat"] ?: "Export Individual Chat") + } + Button( + onClick = { + showExportOptionsDialog = false + exportFullDatabase() + }, + modifier = Modifier.fillMaxWidth(), + colors = sharedButtonColors, + border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)) + ) { + Text(translation["message_logger_export_full_database"] ?: "Export Full Database") + } + } + ) + } + if (showConversationExportDialog) { + AestheticDialog( + onDismissRequest = { dismissConversationExportDialog() }, + title = translation["message_logger_select_chat_title"] ?: "Export Individual Chat", + text = translation["message_logger_select_chat_text"] ?: "Search by username, display name, or chat name.", + icon = Icons.Filled.Search, + confirmButtonText = translation["message_logger_continue_button"] ?: "Continue", + dismissButtonText = context.translation["button.cancel"], + onConfirm = { + val selectedTarget = selectedConversationForExport ?: return@AestheticDialog + pendingConversationExportTarget = selectedTarget + dismissConversationExportDialog() + showConversationFormatDialog = true + }, + onDismiss = { dismissConversationExportDialog() }, + showCloseButton = false, + confirmEnabled = selectedConversationForExport != null, + customContent = { + OutlinedTextField( + value = conversationSearchQuery, + onValueChange = { conversationSearchQuery = it }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text(context.translation["manager.dialogs.add_friend.search_hint"] ?: "Search") + }, + leadingIcon = { + Icon(Icons.Filled.Search, contentDescription = null) + }, + trailingIcon = if (conversationSearchQuery.isNotBlank()) { + { + IconButton(onClick = { conversationSearchQuery = "" }) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = null + ) + } + } + } else null, + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedContainerColor = Color.White.copy(alpha = 0.08f), + unfocusedContainerColor = Color.White.copy(alpha = 0.06f), + focusedTextColor = Color.White, + unfocusedTextColor = Color.White + ) + ) + + if (filteredExportTargets.isEmpty()) { + Text( + text = translation["message_logger_no_chats_found"] ?: "No chats found", + color = PurrfectPalette.textSecondary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } else { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 280.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(filteredExportTargets.size) { index -> + val searchTarget = filteredExportTargets[index] + val target = searchTarget.target + val isSelected = selectedConversationForExport?.conversationId == searchTarget.target.conversationId + OutlinedButton( + onClick = { selectedConversationForExport = searchTarget.target }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White), + border = BorderStroke( + 1.dp, + if (isSelected) { + PurrfectPalette.glowPrimary.copy(alpha = 0.55f) + } else { + Color.White.copy(alpha = 0.18f) + } + ) + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = formatExportTarget(searchTarget), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + val secondaryLabel = when { + searchTarget.friendDisplayName != null && searchTarget.friendUsername != null -> "@${searchTarget.friendUsername}" + searchTarget.friendDisplayName != null -> searchTarget.friendDisplayName + searchTarget.chatDisplayName != null -> searchTarget.chatDisplayName + searchTarget.groupDisplayName != null -> searchTarget.groupDisplayName + searchTarget.readableUsernames.isNotEmpty() -> searchTarget.readableUsernames.joinToString(", ") + else -> null + } + if (secondaryLabel != null) { + Text( + text = secondaryLabel, + color = PurrfectPalette.textSecondary, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Text( + text = translation.format("message_logger_message_count", "count" to target.messageCount.toString()), + color = PurrfectPalette.textSecondary, + fontSize = 12.sp + ) + } + } + } + } + } + } + ) + } + if (showConversationFormatDialog && pendingConversationExportTarget != null) { + AestheticDialog( + onDismissRequest = { dismissConversationFormatDialog() }, + title = translation["message_logger_select_export_format_title"] ?: "Select Export Format", + text = translation["message_logger_select_export_format_text"] ?: "Choose how to export the selected chat.", + icon = Icons.Filled.Description, + confirmButtonText = context.translation["button.cancel"], + onConfirm = { dismissConversationFormatDialog() }, + showCloseButton = false, + customContent = { + exportFormats.forEach { format -> + val formatLabel = when (format.extension) { + "db" -> translation["message_logger_export_format_db"] ?: ".db" + "html" -> translation["message_logger_export_format_html"] ?: "HTML" + else -> translation["message_logger_export_format_txt"] ?: "TXT" + } + Button( + onClick = { + val exportTarget = pendingConversationExportTarget ?: return@Button + dismissConversationFormatDialog() + exportConversation(exportTarget, format) + }, + modifier = Modifier.fillMaxWidth(), + colors = sharedButtonColors, + border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)) + ) { + Text(formatLabel) + } + } + } + ) } } } @@ -1008,9 +1733,15 @@ object LegacyTheme : ThemeContract { ) Text(text = translation["about_tagline"] ?: "", fontSize = 13.sp, color = Color(0xFFD9D3FF), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()) Text(text = translation["about_lead_developers_title"] ?: "Lead Developers", fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = Color.White, modifier = Modifier.padding(top = 10.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically) { - DeveloperCard(name = "ΞTΞRNAL", imageRes = R.drawable.pfp_external, avenirNext = avenirNext, modifier = Modifier.weight(1f)) - DeveloperCard(name = "", imageRes = R.drawable.pfp_rsr, avenirNext = avenirNext, modifier = Modifier.weight(1f)) + Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically) { + DeveloperCard(name = "Eternal", subtitle = "", imageRes = R.drawable.pfp_external, avenirNext = avenirNext, modifier = Modifier.weight(1f)) + DeveloperCard(name = "Kaladin", subtitle = "", imageRes = R.drawable.pfp_kaladin, avenirNext = avenirNext, modifier = Modifier.weight(1f)) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically) { + DeveloperCard(name = "schrodingerspet", subtitle = "", imageRes = R.drawable.pfp_schrodingerspet, avenirNext = avenirNext, modifier = Modifier.weight(1f)) + DeveloperCard(name = "RSR", subtitle = "", imageRes = R.drawable.pfp_rsr, avenirNext = avenirNext, modifier = Modifier.weight(1f)) + } } } } @@ -1072,11 +1803,14 @@ object LegacyTheme : ThemeContract { name: String, imageRes: Int, avenirNext: FontFamily, + subtitle: String? = null, modifier: Modifier = Modifier ) { val tapSource = remember { MutableInteractionSource() } Surface( - modifier = modifier.scaleOnPress(tapSource), + modifier = modifier + .height(150.dp) + .scaleOnPress(tapSource), shape = RoundedCornerShape(22.dp), color = Color.White.copy(alpha = 0.06f), border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f)), @@ -1084,9 +1818,11 @@ object LegacyTheme : ThemeContract { shadowElevation = 0.dp ) { Column( - modifier = Modifier.padding(14.dp), + modifier = Modifier + .fillMaxSize() + .padding(14.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(10.dp) + verticalArrangement = Arrangement.Center ) { Surface( modifier = Modifier.size(64.dp), @@ -1101,15 +1837,28 @@ object LegacyTheme : ThemeContract { modifier = Modifier.fillMaxSize().clip(CircleShape) ) } - PurrfectMarqueeText( + Text( text = name, color = Color.White, - style = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - fontFamily = avenirNext - ) + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + fontFamily = avenirNext, + textAlign = TextAlign.Center, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() ) + subtitle?.takeIf { it.isNotBlank() }?.let { + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = it, + color = Color(0xFFD9D3FF), + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } } } } @@ -1161,62 +1910,69 @@ object LegacyTheme : ThemeContract { fun LogFilterDialog() { androidx.compose.ui.window.Dialog(onDismissRequest = { showFilterDialog = false }) { me.eternal.purrfectsnap.core.ui.PurrfectOverlayTheme { - me.eternal.purrfectsnap.core.ui.PurrfectGlassCard(title = translation["filter_logs_title"] ?: "Filter Log Categories", modifier = Modifier.fillMaxWidth()) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - HomeLogs.LogCategory.entries.forEach { category -> - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .clickable { - // Solo Focus Logic: Tap the name to filter only this category - enabledCategories.keys.forEach { enabledCategories[it] = false } - enabledCategories[category] = true - isRefreshing = true - refreshLogs() + me.eternal.purrfectsnap.core.ui.PurrfectGlassCard( + title = translation["filter_logs_title"] ?: "Log Filters", + modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(14.dp), + color = Color.White.copy(alpha = 0.08f), + border = BorderStroke(1.dp, Color.White.copy(alpha = 0.05f)) + ) { + Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + HomeLogs.LogCategory.entries.forEach { category -> + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickable { + enabledCategories[category] = !(enabledCategories[category] ?: true) + refreshLogs() + } + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = enabledCategories[category] == true, + onCheckedChange = { checked -> + enabledCategories[category] = checked + refreshLogs() + }, + colors = CheckboxDefaults.colors( + checkedColor = PurrfectPalette.glowPrimary, + uncheckedColor = Color.White.copy(alpha = 0.3f), + checkmarkColor = Color.White + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = translation[category.translationKey] ?: category.name, + color = Color.White, + style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold) + ) } - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Checkbox( - checked = enabledCategories[category] == true, - onCheckedChange = { checked -> - enabledCategories[category] = checked - isRefreshing = true - refreshLogs() - }, - colors = CheckboxDefaults.colors( - checkedColor = PurrfectPalette.glowPrimary, - uncheckedColor = Color.White.copy(alpha = 0.4f), - checkmarkColor = Color.White - ) - ) - Text( - text = translation[category.translationKey] ?: category.name, - color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.Medium - ) + } } } - Spacer(modifier = Modifier.height(8.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - Button( - onClick = { showFilterDialog = false }, - shape = RoundedCornerShape(14.dp), - colors = ButtonDefaults.buttonColors(containerColor = PurrfectPalette.glowPrimary) - ) { - Text(translation["filter_logs_done_button"] ?: "Done") - } + Button( + onClick = { showFilterDialog = false }, + modifier = Modifier.fillMaxWidth().height(54.dp), + shape = RoundedCornerShape(18.dp), + colors = ButtonDefaults.buttonColors(containerColor = PurrfectPalette.glowPrimary) + ) { + Text(translation["filter_logs_done_button"] ?: "Apply Filters", fontWeight = FontWeight.Bold, fontSize = 16.sp) } } } } } } - if (showFilterDialog) { LogFilterDialog() } @@ -1296,6 +2052,9 @@ object LegacyTheme : ThemeContract { } @Composable override fun SocialRootSection.SocialScreen(nav: NavBackStackEntry) { + // Controller handles data loading and synchronization + SocialDataController() + val titles = remember { listOf(translation["friends_tab"], translation["groups_tab"]) } @@ -1304,19 +2063,9 @@ object LegacyTheme : ThemeContract { var searchQuery by rememberSaveable { mutableStateOf("") } var searchActive by rememberSaveable { mutableStateOf(false) } - LaunchedEffect(Unit) { - context.database.receiveMessagingDataCallback = { friends, groups -> - friendList = friends - groupList = groups - } - updateScopeLists() - } - DisposableEffect(Unit) { - onDispose { - context.database.receiveMessagingDataCallback = { _, _ -> } - } - } val normalizedQuery = remember(searchQuery) { searchQuery.trim() } + + // Filter logic based on the parent's synchronized data lists val filteredFriends = remember(friendList, normalizedQuery) { if (normalizedQuery.isBlank()) { friendList diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/tracker/ManageFriendTrackerReposSection.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/tracker/ManageFriendTrackerReposSection.kt index 38e1710b..c2390819 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/tracker/ManageFriendTrackerReposSection.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/tracker/ManageFriendTrackerReposSection.kt @@ -269,8 +269,9 @@ class ManageFriendTrackerReposSection: Routes.Route() { } override val content: @Composable (androidx.navigation.NavBackStackEntry) -> Unit = { - val repositories by remember(refreshTrigger.value) { - mutableStateOf>(runBlocking { context.database.getRepositories("friend_tracker") }) + var repositories by remember { mutableStateOf>(emptyList()) } + LaunchedEffect(refreshTrigger.value) { + repositories = context.database.getRepositories("friend_tracker") } val density = LocalDensity.current val statusBarTopPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/SetupActivity.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/SetupActivity.kt index 615bb3c2..86d1afd7 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/SetupActivity.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/SetupActivity.kt @@ -56,6 +56,7 @@ import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Flag import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.SmartToy import androidx.compose.material.icons.filled.VerifiedUser import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Icon @@ -93,6 +94,8 @@ import androidx.navigation.compose.rememberNavController import me.eternal.purrfectsnap.RemoteSideContext import me.eternal.purrfectsnap.SharedContextHolder import me.eternal.purrfectsnap.common.ui.AppMaterialTheme +import me.eternal.purrfectsnap.ui.manager.ManagerAssistantDialog +import me.eternal.purrfectsnap.ui.manager.Routes import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette import me.eternal.purrfectsnap.ui.setup.screens.SetupScreen @@ -104,6 +107,8 @@ import me.eternal.purrfectsnap.ui.setup.screens.impl.PickLanguageScreen import me.eternal.purrfectsnap.ui.setup.screens.impl.PatchSnapchatScreen import me.eternal.purrfectsnap.ui.setup.screens.impl.RootInstallSnapchatScreen import me.eternal.purrfectsnap.ui.setup.screens.impl.SaveFolderScreen +import me.eternal.purrfectsnap.ui.setup.screens.impl.IntroShowcaseScreen +import me.eternal.purrfectsnap.ui.util.ActivityLauncherHelper import me.eternal.purrfectsnap.ui.util.scaleOnPress import kotlinx.coroutines.delay @@ -127,6 +132,9 @@ class SetupActivity : ComponentActivity() { } val requirements = intent.getIntExtra("requirements", Requirements.FIRST_RUN) val setupPrefs = setupContext.sharedPreferences + val setupRoutes = Routes(setupContext).apply { + activityLauncher = ActivityLauncherHelper(this@SetupActivity) + } fun hasRequirement(requirement: Int) = requirements and requirement == requirement val wasInProgress = setupPrefs.getBoolean("setup_in_progress", false) val isFirstRunFlow = hasRequirement(Requirements.FIRST_RUN) || wasInProgress @@ -159,6 +167,7 @@ class SetupActivity : ComponentActivity() { val requiredScreens = mutableListOf().apply { if (isFirstRunFlow || hasRequirement(Requirements.LANGUAGE)) { + add(IntroShowcaseScreen().apply { route = "introShowcase" }) add(PickLanguageScreen().apply { route = "language" }) if (isFirstRunFlow) { add(InstallModeScreen( @@ -315,19 +324,7 @@ class SetupActivity : ComponentActivity() { AppMaterialTheme { val view = LocalView.current val navBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - var showImportantDialog by rememberSaveable { - mutableStateOf(!setupPrefs.getBoolean("setup_important_notice_shown", false)) - } - var importantTimeout by remember { mutableIntStateOf(5) } - LaunchedEffect(showImportantDialog) { - if (showImportantDialog) { - importantTimeout = 5 - while (importantTimeout > 0) { - delay(1000) - importantTimeout-- - } - } - } + var setupAiPrompt by rememberSaveable { mutableStateOf(null) } SideEffect { val window = (view.context as Activity).window WindowCompat.setDecorFitsSystemWindows(window, false) @@ -344,46 +341,8 @@ class SetupActivity : ComponentActivity() { .fillMaxSize() .background(Color.Transparent) ) { - if (showImportantDialog) { - val confirmLabel = if (importantTimeout > 0) { - translation.format( - "setup.activity.important_confirm_timeout", - "seconds" to importantTimeout.toString() - ) - } else { - translation["setup.activity.important_confirm"] - } - AestheticDialog( - onDismissRequest = { - if (importantTimeout == 0) { - showImportantDialog = false - setupPrefs.edit().putBoolean("setup_important_notice_shown", true).apply() - } - }, - title = translation["setup.activity.important_title"], - text = "", - icon = Icons.Filled.Warning, - confirmButtonText = confirmLabel, - onConfirm = { - if (importantTimeout == 0) { - showImportantDialog = false - setupPrefs.edit().putBoolean("setup_important_notice_shown", true).apply() - } - }, - confirmEnabled = importantTimeout == 0, - showCloseButton = false, - customContent = { - Text( - text = translation["setup.activity.important_message"], - color = PurrfectPalette.textSecondary, - lineHeight = 18.sp - ) - }, - opaque = true - ) - } SetupAuroraBackground() - SetupTopBar() + SetupTopBar(onAskAi = { setupAiPrompt = "hi" }) val bottomPadding = 118.dp + navBarPadding Column( modifier = Modifier @@ -468,6 +427,14 @@ class SetupActivity : ComponentActivity() { .navigationBarsPadding() .padding(bottom = 32.dp) ) + setupAiPrompt?.let { prompt -> + ManagerAssistantDialog( + context = setupContext, + routes = setupRoutes, + initialUserMessage = prompt, + onDismiss = { setupAiPrompt = null } + ) + } } } } @@ -483,6 +450,12 @@ private fun SetupScreen.meta(context: RemoteSideContext): SetupStepMeta { subtitle = translation["setup.activity.language_subtitle"], icon = Icons.Filled.Language ) + is IntroShowcaseScreen -> SetupStepMeta( + route = route, + title = "Welcome", + subtitle = "Preview what PurrfectSnap can do", + icon = Icons.Filled.AutoAwesome + ) is InstallModeScreen -> SetupStepMeta( route = route, @@ -587,7 +560,7 @@ private fun SetupAuroraBackground() { } @Composable -private fun SetupTopBar() { +private fun SetupTopBar(onAskAi: () -> Unit) { val topPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() Surface( modifier = Modifier @@ -613,14 +586,37 @@ private fun SetupTopBar() { .fillMaxWidth() .padding(horizontal = 18.dp, vertical = 16.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { Text( text = "PurrfectSnap", color = PurrfectPalette.textPrimary, fontWeight = FontWeight.ExtraBold, - fontSize = 18.sp + fontSize = 18.sp, + modifier = Modifier.weight(1f) ) + Surface( + shape = RoundedCornerShape(40), + color = Color.White.copy(alpha = 0.08f), + border = androidx.compose.foundation.BorderStroke(1.dp, Color.White.copy(alpha = 0.14f)) + ) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(40)) + .clickable(onClick = onAskAi) + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon(Icons.Filled.SmartToy, contentDescription = null, tint = Color.White) + Text( + text = "Ask AI", + color = Color.White, + fontWeight = FontWeight.Medium, + fontSize = 13.sp + ) + } + } } } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/screens/impl/IntroShowcaseScreen.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/screens/impl/IntroShowcaseScreen.kt new file mode 100644 index 00000000..d0abd59a --- /dev/null +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/screens/impl/IntroShowcaseScreen.kt @@ -0,0 +1,112 @@ +package me.eternal.purrfectsnap.ui.setup.screens.impl + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay +import me.eternal.purrfectsnap.R +import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette +import me.eternal.purrfectsnap.ui.setup.screens.SetupScreen + +class IntroShowcaseScreen : SetupScreen() { + private val slides = listOf( + R.drawable.setup_slide_plus to "Unlock Snapchat Plus for free!", + R.drawable.setup_slide_upload_tag to "Bypass the Media Upload tag!", + R.drawable.setup_slide_downloads to "Download Snaps, & Spotlights!" + ) + + @Composable + override fun Content() { + LaunchedEffect(Unit) { allowNext(true) } + var currentIndex by remember { mutableIntStateOf(0) } + + LaunchedEffect(Unit) { + while (true) { + delay(5000) + currentIndex = (currentIndex + 1) % slides.size + } + } + + SetupCard { + StepTitle( + title = "Welcome to PurrfectSnap", + subtitle = "A quick look before setup begins", + modifier = Modifier.align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center + ) + AnimatedContent(targetState = currentIndex, label = "setupShowcase") { index -> + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + Surface( + modifier = Modifier + .fillMaxWidth() + .height(260.dp), + shape = RoundedCornerShape(24.dp), + color = PurrfectPalette.cardOverlayColor, + border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)) + ) { + Image( + painter = painterResource(slides[index].first), + contentDescription = slides[index].second, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(24.dp)) + ) + } + Text( + text = slides[index].second, + color = Color.White, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } + } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + slides.forEachIndexed { index, _ -> + Box( + modifier = Modifier + .size(if (index == currentIndex) 10.dp else 8.dp) + .clip(CircleShape) + .background(if (index == currentIndex) PurrfectPalette.glowPrimary else Color.White.copy(alpha = 0.3f)) + ) + } + } + } + } +} diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/screens/impl/PatchSnapchatScreen.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/screens/impl/PatchSnapchatScreen.kt index 43786491..1734a747 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/screens/impl/PatchSnapchatScreen.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/setup/screens/impl/PatchSnapchatScreen.kt @@ -74,6 +74,8 @@ import kotlinx.coroutines.withContext import me.eternal.purrfectsnap.common.bridge.wrapper.LocaleWrapper import me.eternal.purrfectsnap.setup.patch.AutoPatchServer import me.eternal.purrfectsnap.setup.patch.LSPatch +import me.eternal.purrfectsnap.ui.manager.ManagerAssistantDialog +import me.eternal.purrfectsnap.ui.manager.Routes import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette import me.eternal.purrfectsnap.ui.setup.screens.SetupScreen @@ -111,6 +113,10 @@ class PatchSnapchatScreen : SetupScreen() { var installWatcher by remember { mutableStateOf(null) } var downloadFinished by rememberSaveable { mutableStateOf(false) } var showIssuesDialog by remember { mutableStateOf(false) } + val assistantRoutes = remember { + Routes(context).apply { + } + } val logPulse by rememberInfiniteTransition(label = "logPulse").animateFloat( initialValue = 0f, targetValue = 1f, @@ -313,62 +319,12 @@ class PatchSnapchatScreen : SetupScreen() { } if (showIssuesDialog) { - AestheticDialog( - onDismissRequest = { showIssuesDialog = false }, - title = translation["setup.patch.issues_title"], - text = "", - icon = Icons.Filled.Info, - confirmButtonText = translation["setup.patch.issues_confirm"], - onConfirm = { showIssuesDialog = false }, - showCloseButton = false, - customContent = { - val bodyStyle = MaterialTheme.typography.bodyMedium.copy( - color = PurrfectPalette.textSecondary, - lineHeight = 18.sp - ) - Column( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 360.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - Text( - text = translation["setup.patch.issues_heading"], - fontWeight = FontWeight.SemiBold, - color = Color.White, - textAlign = TextAlign.Start, - modifier = Modifier.fillMaxWidth() - ) - Text( - text = translation["setup.patch.issues_conflict_issue"], - style = bodyStyle, - textAlign = TextAlign.Start - ) - Text( - text = translation["setup.patch.issues_conflict_fix"], - style = bodyStyle, - textAlign = TextAlign.Start - ) - Text( - text = translation["setup.patch.issues_adb_command"], - style = bodyStyle, - textAlign = TextAlign.Start, - softWrap = false, - modifier = Modifier.horizontalScroll(rememberScrollState()) - ) - Text( - text = translation["setup.patch.issues_invalid_issue"], - style = bodyStyle, - textAlign = TextAlign.Start - ) - Text( - text = translation["setup.patch.issues_invalid_fix"], - style = bodyStyle, - textAlign = TextAlign.Start - ) - } - } + ManagerAssistantDialog( + context = context, + routes = assistantRoutes, + initialUserMessage = "I am facing an App not installed issue or Package appears to be invalid issue while installing Snapchat. How do I fix it?", + showImprovementLogging = false, + onDismiss = { showIssuesDialog = false } ) } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/AlertDialogs.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/AlertDialogs.kt index b46892e9..bc242878 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/AlertDialogs.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/util/AlertDialogs.kt @@ -431,9 +431,10 @@ class AlertDialogs( DefaultDialogCard { var fieldValue by remember { mutableStateOf(property.value.get().toString().let { + val t = if (property.key.params.digitsOnlyInput) it.filter { ch -> ch.isDigit() } else it TextFieldValue( - text = it, - selection = TextRange(it.length) + text = t, + selection = TextRange(t.length) ) }) } @@ -447,10 +448,21 @@ class AlertDialogs( } .focusRequester(focusRequester), value = fieldValue, - onValueChange = { fieldValue = it }, - keyboardOptions = when (property.key.dataType.type) { - DataProcessors.Type.INTEGER -> KeyboardOptions(keyboardType = KeyboardType.Number) - DataProcessors.Type.FLOAT -> KeyboardOptions(keyboardType = KeyboardType.Decimal) + onValueChange = { newVal -> + fieldValue = if (property.key.params.digitsOnlyInput) { + val filtered = newVal.text.filter { ch -> ch.isDigit() } + if (newVal.text != filtered) { + Toast.makeText(context, translation["manager.sections.features.digits_only_toast"], Toast.LENGTH_SHORT).show() + } + newVal.copy(text = filtered) + } else { + newVal + } + }, + keyboardOptions = when { + property.key.params.digitsOnlyInput -> KeyboardOptions(keyboardType = KeyboardType.Number) + property.key.dataType.type == DataProcessors.Type.INTEGER -> KeyboardOptions(keyboardType = KeyboardType.Number) + property.key.dataType.type == DataProcessors.Type.FLOAT -> KeyboardOptions(keyboardType = KeyboardType.Decimal) else -> KeyboardOptions(keyboardType = KeyboardType.Text) }, singleLine = true, diff --git a/app/src/main/res/drawable/pfp_kaladin.jpg b/app/src/main/res/drawable/pfp_kaladin.jpg new file mode 100644 index 00000000..edd40029 Binary files /dev/null and b/app/src/main/res/drawable/pfp_kaladin.jpg differ diff --git a/app/src/main/res/drawable/pfp_schrodingerspet.jpg b/app/src/main/res/drawable/pfp_schrodingerspet.jpg new file mode 100644 index 00000000..bec7a33c Binary files /dev/null and b/app/src/main/res/drawable/pfp_schrodingerspet.jpg differ diff --git a/app/src/main/res/drawable/setup_slide_downloads.jpg b/app/src/main/res/drawable/setup_slide_downloads.jpg new file mode 100644 index 00000000..a1866907 Binary files /dev/null and b/app/src/main/res/drawable/setup_slide_downloads.jpg differ diff --git a/app/src/main/res/drawable/setup_slide_plus.jpg b/app/src/main/res/drawable/setup_slide_plus.jpg new file mode 100644 index 00000000..131df1bc Binary files /dev/null and b/app/src/main/res/drawable/setup_slide_plus.jpg differ diff --git a/app/src/main/res/drawable/setup_slide_upload_tag.jpg b/app/src/main/res/drawable/setup_slide_upload_tag.jpg new file mode 100644 index 00000000..50df09bb Binary files /dev/null and b/app/src/main/res/drawable/setup_slide_upload_tag.jpg differ diff --git a/build.gradle.kts b/build.gradle.kts index c43112cf..341e38e3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,8 +33,8 @@ tasks.register("getVersion") { } // You can still set these for legacy use by submodules or scripts: -rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.9").get()) -rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("325").get().toInt()) +rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.7.1").get()) +rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("327").get().toInt()) rootProject.ext.set("applicationId", "me.eternal.purrfectsnap") // buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate. // Include version code so each release has a different hash; use random for uniqueness within same version. diff --git a/changelogs-prerelease.txt b/changelogs-prerelease.txt index b8540880..8ae3f0a2 100644 --- a/changelogs-prerelease.txt +++ b/changelogs-prerelease.txt @@ -1,4 +1,5 @@ ## v1.1.0 +- Fix: Download Context Menu now resolves Message Logger deleted media when available. - Fix: message preview overlap in French. - Fix: transparent gallery media send override dialog. - Fix: message logger autopurge "Never" no longer resets to 3 days. diff --git a/changelogs-stable.txt b/changelogs-stable.txt index 05fca07a..4a4162e7 100644 --- a/changelogs-stable.txt +++ b/changelogs-stable.txt @@ -1,3 +1,43 @@ +## v1.7.1 +- Auto-Open Engine ghost notification fix. +- Continous Send notifiction bug fix. +- Disappering chats fix. +- Social page automatic selection bug fix. + +## v1.7.0 +- Features: +- Implemented "PurrfectSnap AI" (tq ΞTΞRNAL) +- Implemented app intro showcase (tq ΞTΞRNAL) +- Implemented "Spoof follower count" (tq RSR) +- Implemented Social Tab sorting by Streak Length (tq Javalsta) +- Implemented "Chat Hold Kill" (tq SUJΛL) +- Implemented "Snapchat plus purchase date spoof" (tq SUJΛL) +- Implemented Message log export for individual chat (tq 𝚜𝚌𝚑𝚛𝚘𝚍𝚒𝚗𝚐𝚎𝚛𝚜𝚙𝚎𝚝) +- Implemented Memory message icon indicator(tq RSR) +- Implemented two new message indicator toggles, for self snaps and group. +- Implemented new toggles for chat and snap stealth mode in friend feed menu. +- Implemented new notification card for Continous send feature. + +- Fixes: +- Improved message icon indicator reliability and redesigned all chat status indicators. (tq RSR) +- Media resend flow bug fixes. (tq 𝚜𝚌𝚑𝚛𝚘𝚍𝚒𝚗𝚐𝚎𝚛𝚜𝚙𝚎𝚝) +- Message Logger backup import bug fixes and implemented logging to report success or failure. (tq 𝚜𝚌𝚑𝚛𝚘𝚍𝚒𝚗𝚐𝚎𝚛𝚜𝚙𝚎𝚝) +- Media download support through message logger. (tq 𝚜𝚌𝚑𝚛𝚘𝚍𝚒𝚗𝚐𝚎𝚛𝚜𝚙𝚎𝚝) +- Snapchat Plus bug fixes to improve stability. +- Spoof Device profile Backup/Restore bug fixes. +- Redesgined manager app Logs filtering UI. +- Media Downloader stability and bug fixes. +- Performance Mode: Fixed Max Performance mode persistence after app restart. +- Chat Preview: Realignment of the chat preview layout. +- Update Checker: Refactored the app update checker to follow a single, stable release track with automated daily checks. +- Auto-Open engine: Expanded Notification UI has been refactored and redundant stats has been removed. +- Social tab: Converted the rules configuration page to a Lazy Column architecture. This eliminates UI freezes when managing large friend lists. +- Chat feed scroll stutter: Implemented a thread-safe memory cache for chat suppression logic. This instant memory check allows Snapchat chat feed to scroll at 100% native speed. +- GPU Offloading: Forced hardware-layer acceleration for all camera and video preview views to reduce CPU thermal load and power consumption. +- Refactored all runBlocking calls and main-thread database queries across 12 major pages to eliminate the UI freezes/stutters and the scrolling, navigation, and page transitions are now locked at high frame rates. +- Migrated heavy bridge data processing (JSON parsing and DB writes) to background IO coroutines, preventing ANR (App Not Responding) crashes during large data synchronizations. + + ## v1.6.9 - New: Updated the Stealth mode for better visibility with the chat stealth mode (keeps chats from being read), and snap stealth-mode and full stealth mode toggle (normal stealth-mode). (tq Javalsta) - Fix: Fixed performance mode profile save/load so Disabled persists correctly and no longer falls back to Max mode on app restart. (tq schrodingerspet) diff --git a/common/src/main/aidl/me/eternal/purrfectsnap/bridge/BridgeInterface.aidl b/common/src/main/aidl/me/eternal/purrfectsnap/bridge/BridgeInterface.aidl index f86ac18e..98e7dc45 100644 --- a/common/src/main/aidl/me/eternal/purrfectsnap/bridge/BridgeInterface.aidl +++ b/common/src/main/aidl/me/eternal/purrfectsnap/bridge/BridgeInterface.aidl @@ -71,7 +71,7 @@ interface BridgeInterface { * @param groups list of groups (MessagingGroupInfo as parcelable) * @param friends list of friends (MessagingFriendInfo as parcelable) */ - oneway void passGroupsAndFriends(in List groups, in List friends); + oneway void passGroupsAndFriends(in List groups, in List friends, int chunkIndex, int totalChunks); @nullable String getScopeNotes(String id); diff --git a/common/src/main/assets/lang/ar_AE.json b/common/src/main/assets/lang/ar_AE.json index 77c0e964..a833bcc1 100644 --- a/common/src/main/assets/lang/ar_AE.json +++ b/common/src/main/assets/lang/ar_AE.json @@ -1,2616 +1,2632 @@ { "setup": { "activity": { - "wrong_apk_title": "تم تثبيت APK خاطئ", - "wrong_apk_message": "جهازك يعمل بمعمارية armv8، يرجى تحميل ملف apk الخاص بـ armv8، وليس armv7.", - "close_button": "إغلاق", - "important_confirm_timeout": "أنا أفهم ({seconds}ث)", - "important_confirm": "أنا أفهم", - "important_title": "مهم!", - "important_message": "إذا كنت قد استخدمت SnapEnhance أو أي تعديل آخر غير PurrfectSnap، نوصي بإلغاء تثبيت كل شيء والبقاء على تطبيق Snapchat الرسمي لمدة أسبوع واحد. ثم انتقل إلى PurrfectSnap بعد يوم الجمعة القادم.", - "language_subtitle": "اضبط PurrfectSnap ليتحدث لغتك قبل أي شيء آخر.", - "install_mode_title": "اختر جهازك", - "install_mode_subtitle": "اختر المسار الذي يطابق طريقة تثبيتك لـ PurrfectSnap.", - "permissions_subtitle": "امنح الأذونات الأساسية لتظل التراكبات والتنزيلات والتنبيهات موثوقة.", - "patch_title": "المصحح التلقائي (Auto Patcher)", - "patch_subtitle": "تنزيل وتصحيح وتثبيت مبسط في عملية واحدة.", - "root_install_title": "مثبت Snapchat", - "root_install_subtitle": "تنزيل وتثبيت إصدار Snapchat الموصى به.", - "save_folder_subtitle": "اختر خزانتك الشخصية لتستقر الـ Snaps في المكان الذي تتوقعه تماماً.", - "mappings_subtitle": "نقوم بمعايرة كل شيء لتثبيتك ليعمل السحر بلا شائبة.", - "step_counter": "الخطوة {current} من {total}", - "step_complete": "تم التحقق", - "step_active": "قيد التقدم", - "step_upcoming": "جاهز تالياً", - "finish_button": "إنهاء الإعداد", - "continue_button": "متابعة" + "wrong_apk_title": "\u062a\u0645 \u062a\u062b\u0628\u064a\u062a APK \u062e\u0627\u0637\u0626", + "wrong_apk_message": "\u062c\u0647\u0627\u0632\u0643 \u064a\u0639\u0645\u0644 \u0628\u0645\u0639\u0645\u0627\u0631\u064a\u0629 armv8\u060c \u064a\u0631\u062c\u0649 \u062a\u062d\u0645\u064a\u0644 \u0645\u0644\u0641 apk \u0627\u0644\u062e\u0627\u0635 \u0628\u0640 armv8\u060c \u0648\u0644\u064a\u0633 armv7.", + "close_button": "\u0625\u063a\u0644\u0627\u0642", + "important_confirm_timeout": "\u0623\u0646\u0627 \u0623\u0641\u0647\u0645 ({seconds}\u062b)", + "important_confirm": "\u0623\u0646\u0627 \u0623\u0641\u0647\u0645", + "important_title": "\u0645\u0647\u0645!", + "important_message": "\u0625\u0630\u0627 \u0643\u0646\u062a \u0642\u062f \u0627\u0633\u062a\u062e\u062f\u0645\u062a SnapEnhance \u0623\u0648 \u0623\u064a \u062a\u0639\u062f\u064a\u0644 \u0622\u062e\u0631 \u063a\u064a\u0631 PurrfectSnap\u060c \u0646\u0648\u0635\u064a \u0628\u0625\u0644\u063a\u0627\u0621 \u062a\u062b\u0628\u064a\u062a \u0643\u0644 \u0634\u064a\u0621 \u0648\u0627\u0644\u0628\u0642\u0627\u0621 \u0639\u0644\u0649 \u062a\u0637\u0628\u064a\u0642 Snapchat \u0627\u0644\u0631\u0633\u0645\u064a \u0644\u0645\u062f\u0629 \u0623\u0633\u0628\u0648\u0639 \u0648\u0627\u062d\u062f. \u062b\u0645 \u0627\u0646\u062a\u0642\u0644 \u0625\u0644\u0649 PurrfectSnap \u0628\u0639\u062f \u064a\u0648\u0645 \u0627\u0644\u062c\u0645\u0639\u0629 \u0627\u0644\u0642\u0627\u062f\u0645.", + "language_subtitle": "\u0627\u0636\u0628\u0637 PurrfectSnap \u0644\u064a\u062a\u062d\u062f\u062b \u0644\u063a\u062a\u0643 \u0642\u0628\u0644 \u0623\u064a \u0634\u064a\u0621 \u0622\u062e\u0631.", + "install_mode_title": "\u0627\u062e\u062a\u0631 \u062c\u0647\u0627\u0632\u0643", + "install_mode_subtitle": "\u0627\u062e\u062a\u0631 \u0627\u0644\u0645\u0633\u0627\u0631 \u0627\u0644\u0630\u064a \u064a\u0637\u0627\u0628\u0642 \u0637\u0631\u064a\u0642\u0629 \u062a\u062b\u0628\u064a\u062a\u0643 \u0644\u0640 PurrfectSnap.", + "permissions_subtitle": "\u0627\u0645\u0646\u062d \u0627\u0644\u0623\u0630\u0648\u0646\u0627\u062a \u0627\u0644\u0623\u0633\u0627\u0633\u064a\u0629 \u0644\u062a\u0638\u0644 \u0627\u0644\u062a\u0631\u0627\u0643\u0628\u0627\u062a \u0648\u0627\u0644\u062a\u0646\u0632\u064a\u0644\u0627\u062a \u0648\u0627\u0644\u062a\u0646\u0628\u064a\u0647\u0627\u062a \u0645\u0648\u062b\u0648\u0642\u0629.", + "patch_title": "\u0627\u0644\u0645\u0635\u062d\u062d \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a (Auto Patcher)", + "patch_subtitle": "\u062a\u0646\u0632\u064a\u0644 \u0648\u062a\u0635\u062d\u064a\u062d \u0648\u062a\u062b\u0628\u064a\u062a \u0645\u0628\u0633\u0637 \u0641\u064a \u0639\u0645\u0644\u064a\u0629 \u0648\u0627\u062d\u062f\u0629.", + "root_install_title": "\u0645\u062b\u0628\u062a Snapchat", + "root_install_subtitle": "\u062a\u0646\u0632\u064a\u0644 \u0648\u062a\u062b\u0628\u064a\u062a \u0625\u0635\u062f\u0627\u0631 Snapchat \u0627\u0644\u0645\u0648\u0635\u0649 \u0628\u0647.", + "save_folder_subtitle": "\u0627\u062e\u062a\u0631 \u062e\u0632\u0627\u0646\u062a\u0643 \u0627\u0644\u0634\u062e\u0635\u064a\u0629 \u0644\u062a\u0633\u062a\u0642\u0631 \u0627\u0644\u0640 Snaps \u0641\u064a \u0627\u0644\u0645\u0643\u0627\u0646 \u0627\u0644\u0630\u064a \u062a\u062a\u0648\u0642\u0639\u0647 \u062a\u0645\u0627\u0645\u0627\u064b.", + "mappings_subtitle": "\u0646\u0642\u0648\u0645 \u0628\u0645\u0639\u0627\u064a\u0631\u0629 \u0643\u0644 \u0634\u064a\u0621 \u0644\u062a\u062b\u0628\u064a\u062a\u0643 \u0644\u064a\u0639\u0645\u0644 \u0627\u0644\u0633\u062d\u0631 \u0628\u0644\u0627 \u0634\u0627\u0626\u0628\u0629.", + "step_counter": "\u0627\u0644\u062e\u0637\u0648\u0629 {current} \u0645\u0646 {total}", + "step_complete": "\u062a\u0645 \u0627\u0644\u062a\u062d\u0642\u0642", + "step_active": "\u0642\u064a\u062f \u0627\u0644\u062a\u0642\u062f\u0645", + "step_upcoming": "\u062c\u0627\u0647\u0632 \u062a\u0627\u0644\u064a\u0627\u064b", + "finish_button": "\u0625\u0646\u0647\u0627\u0621 \u0627\u0644\u0625\u0639\u062f\u0627\u062f", + "continue_button": "\u0645\u062a\u0627\u0628\u0639\u0629" }, "dialogs": { - "select_language": "اختر اللغة", - "save_folder": "اختر مكان حفظ التنزيلات", - "select_save_folder_button": "اختر المجلد", - "hex_color_label": "لون Hex" + "select_language": "\u0627\u062e\u062a\u0631 \u0627\u0644\u0644\u063a\u0629", + "save_folder": "\u0627\u062e\u062a\u0631 \u0645\u0643\u0627\u0646 \u062d\u0641\u0638 \u0627\u0644\u062a\u0646\u0632\u064a\u0644\u0627\u062a", + "select_save_folder_button": "\u0627\u062e\u062a\u0631 \u0627\u0644\u0645\u062c\u0644\u062f", + "hex_color_label": "\u0644\u0648\u0646 Hex" }, "install_mode": { - "confirm_timeout": "أنا أفهم ({seconds}ث)", - "confirm": "أنا أفهم", - "notice_title": "يرجى الملاحظة!", - "notice_intro": "حدد نوع جهازك: بصلاحيات روت (Rooted) أو بدون روت. إذا كنت غير متأكد، اختر بدون روت وتابع.", - "notice_non_root_title": "الأجهزة بدون روت", - "notice_non_root_body": "اختر Non-root وسيتولى التطبيق كل شيء. اضغط على 'تثبيت Snapchat المعدل' (Install Patched Snapchat) عندما يظهر. بعد التثبيت، لا تفتح Snapchat بعد. تابع إعداد PurrfectSnap؛ بمجرد الانتهاء، يمكنك فتح Snapchat والاستمتاع.", - "notice_root_title": "الأجهزة بصلاحيات روت", - "notice_root_body": "تأكد من تثبيت (Flash) LSPosed أولاً. نوصي بـ JingMatrix LSPosed أو LSPosed Irena. بعد اختيار Root، سيقوم التطبيق بتثبيت إصدار Snapchat الموصى به. لا تفتحه بعد؛ تابع إعداد PurrfectSnap. عند انتهاء الإعداد، قم بتمكين PurrfectSnap في LSPosed وأعد تشغيل هاتفك. ثم ابدأ باستخدام Snapchat. نوصي بشدة بفصل Snapchat عن متجر Play باستخدام وحدة Zygisk Detach لمنع التحديثات التلقائية.", - "notice_issues_hint": "إذا واجهت أي مشاكل في التثبيت، سيظهر الحل هنا. يرجى قراءته بعناية.", - "notice_note_prefix": "ملاحظة: ", - "notice_note_body": "الحسابات الجديدة يتم قفلها بسهولة! يوصى باستخدام حساب قديم مع PurrfectSnap.", - "step_title": "اختر جهازك", - "step_subtitle": "إذا كنت لا تعرف، اختر جهاز بدون روت (Non-rooted) وتابع.", - "root_option_title": "جهاز بصلاحيات روت (Rooted)", - "root_option_subtitle": "استخدم Lsposed وتخطى التصحيح التلقائي.", - "non_root_option_title": "جهاز بدون روت (Non-rooted)", - "non_root_option_subtitle": "استخدم المصحح التلقائي المضمن لتثبيت Snapchat المعدل.", - "skip_auto_setup": "تخطي الإعداد التلقائي" + "confirm_timeout": "\u0623\u0646\u0627 \u0623\u0641\u0647\u0645 ({seconds}\u062b)", + "confirm": "\u0623\u0646\u0627 \u0623\u0641\u0647\u0645", + "notice_title": "\u064a\u0631\u062c\u0649 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0629!", + "notice_intro": "\u062d\u062f\u062f \u0646\u0648\u0639 \u062c\u0647\u0627\u0632\u0643: \u0628\u0635\u0644\u0627\u062d\u064a\u0627\u062a \u0631\u0648\u062a (Rooted) \u0623\u0648 \u0628\u062f\u0648\u0646 \u0631\u0648\u062a. \u0625\u0630\u0627 \u0643\u0646\u062a \u063a\u064a\u0631 \u0645\u062a\u0623\u0643\u062f\u060c \u0627\u062e\u062a\u0631 \u0628\u062f\u0648\u0646 \u0631\u0648\u062a \u0648\u062a\u0627\u0628\u0639.", + "notice_non_root_title": "\u0627\u0644\u0623\u062c\u0647\u0632\u0629 \u0628\u062f\u0648\u0646 \u0631\u0648\u062a", + "notice_non_root_body": "\u0627\u062e\u062a\u0631 Non-root \u0648\u0633\u064a\u062a\u0648\u0644\u0649 \u0627\u0644\u062a\u0637\u0628\u064a\u0642 \u0643\u0644 \u0634\u064a\u0621. \u0627\u0636\u063a\u0637 \u0639\u0644\u0649 '\u062a\u062b\u0628\u064a\u062a Snapchat \u0627\u0644\u0645\u0639\u062f\u0644' (Install Patched Snapchat) \u0639\u0646\u062f\u0645\u0627 \u064a\u0638\u0647\u0631. \u0628\u0639\u062f \u0627\u0644\u062a\u062b\u0628\u064a\u062a\u060c \u0644\u0627 \u062a\u0641\u062a\u062d Snapchat \u0628\u0639\u062f. \u062a\u0627\u0628\u0639 \u0625\u0639\u062f\u0627\u062f PurrfectSnap\u061b \u0628\u0645\u062c\u0631\u062f \u0627\u0644\u0627\u0646\u062a\u0647\u0627\u0621\u060c \u064a\u0645\u0643\u0646\u0643 \u0641\u062a\u062d Snapchat \u0648\u0627\u0644\u0627\u0633\u062a\u0645\u062a\u0627\u0639.", + "notice_root_title": "\u0627\u0644\u0623\u062c\u0647\u0632\u0629 \u0628\u0635\u0644\u0627\u062d\u064a\u0627\u062a \u0631\u0648\u062a", + "notice_root_body": "\u062a\u0623\u0643\u062f \u0645\u0646 \u062a\u062b\u0628\u064a\u062a (Flash) LSPosed \u0623\u0648\u0644\u0627\u064b. \u0646\u0648\u0635\u064a \u0628\u0640 JingMatrix LSPosed \u0623\u0648 LSPosed Irena. \u0628\u0639\u062f \u0627\u062e\u062a\u064a\u0627\u0631 Root\u060c \u0633\u064a\u0642\u0648\u0645 \u0627\u0644\u062a\u0637\u0628\u064a\u0642 \u0628\u062a\u062b\u0628\u064a\u062a \u0625\u0635\u062f\u0627\u0631 Snapchat \u0627\u0644\u0645\u0648\u0635\u0649 \u0628\u0647. \u0644\u0627 \u062a\u0641\u062a\u062d\u0647 \u0628\u0639\u062f\u061b \u062a\u0627\u0628\u0639 \u0625\u0639\u062f\u0627\u062f PurrfectSnap. \u0639\u0646\u062f \u0627\u0646\u062a\u0647\u0627\u0621 \u0627\u0644\u0625\u0639\u062f\u0627\u062f\u060c \u0642\u0645 \u0628\u062a\u0645\u0643\u064a\u0646 PurrfectSnap \u0641\u064a LSPosed \u0648\u0623\u0639\u062f \u062a\u0634\u063a\u064a\u0644 \u0647\u0627\u062a\u0641\u0643. \u062b\u0645 \u0627\u0628\u062f\u0623 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 Snapchat. \u0646\u0648\u0635\u064a \u0628\u0634\u062f\u0629 \u0628\u0641\u0635\u0644 Snapchat \u0639\u0646 \u0645\u062a\u062c\u0631 Play \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0648\u062d\u062f\u0629 Zygisk Detach \u0644\u0645\u0646\u0639 \u0627\u0644\u062a\u062d\u062f\u064a\u062b\u0627\u062a \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a\u0629.", + "notice_issues_hint": "\u0625\u0630\u0627 \u0648\u0627\u062c\u0647\u062a \u0623\u064a \u0645\u0634\u0627\u0643\u0644 \u0641\u064a \u0627\u0644\u062a\u062b\u0628\u064a\u062a\u060c \u0633\u064a\u0638\u0647\u0631 \u0627\u0644\u062d\u0644 \u0647\u0646\u0627. \u064a\u0631\u062c\u0649 \u0642\u0631\u0627\u0621\u062a\u0647 \u0628\u0639\u0646\u0627\u064a\u0629.", + "notice_note_prefix": "\u0645\u0644\u0627\u062d\u0638\u0629: ", + "notice_note_body": "\u0627\u0644\u062d\u0633\u0627\u0628\u0627\u062a \u0627\u0644\u062c\u062f\u064a\u062f\u0629 \u064a\u062a\u0645 \u0642\u0641\u0644\u0647\u0627 \u0628\u0633\u0647\u0648\u0644\u0629! \u064a\u0648\u0635\u0649 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u062d\u0633\u0627\u0628 \u0642\u062f\u064a\u0645 \u0645\u0639 PurrfectSnap.", + "step_title": "\u0627\u062e\u062a\u0631 \u062c\u0647\u0627\u0632\u0643", + "step_subtitle": "\u0625\u0630\u0627 \u0643\u0646\u062a \u0644\u0627 \u062a\u0639\u0631\u0641\u060c \u0627\u062e\u062a\u0631 \u062c\u0647\u0627\u0632 \u0628\u062f\u0648\u0646 \u0631\u0648\u062a (Non-rooted) \u0648\u062a\u0627\u0628\u0639.", + "root_option_title": "\u062c\u0647\u0627\u0632 \u0628\u0635\u0644\u0627\u062d\u064a\u0627\u062a \u0631\u0648\u062a (Rooted)", + "root_option_subtitle": "\u0627\u0633\u062a\u062e\u062f\u0645 Lsposed \u0648\u062a\u062e\u0637\u0649 \u0627\u0644\u062a\u0635\u062d\u064a\u062d \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a.", + "non_root_option_title": "\u062c\u0647\u0627\u0632 \u0628\u062f\u0648\u0646 \u0631\u0648\u062a (Non-rooted)", + "non_root_option_subtitle": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u0645\u0635\u062d\u062d \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0627\u0644\u0645\u0636\u0645\u0646 \u0644\u062a\u062b\u0628\u064a\u062a Snapchat \u0627\u0644\u0645\u0639\u062f\u0644.", + "skip_auto_setup": "\u062a\u062e\u0637\u064a \u0627\u0644\u0625\u0639\u062f\u0627\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a" }, "mappings": { - "dialog": "جاري إنشاء التعيينات (Mappings)...", - "generate_failure_no_snapchat": "لم يتمكن PurrfectSnap من اكتشاف Snapchat، يرجى محاولة إعادة تثبيت Snapchat.", - "generate_failure": "حدث خطأ أثناء محاولة إنشاء التعيينات، يرجى المحاولة مرة أخرى.", - "confirm_understand_timeout": "أنا أفهم ({seconds}ث)", - "confirm_understand": "أنا أفهم", - "notice_title": "يرجى الملاحظة!", - "notice_intro": "إذا رأيت خطأ \"الحساب معطل مؤقتاً\" أثناء تسجيل الدخول، لا تقلق. اتبع هذه الخطوات بالترتيب:", - "notice_step_1": "1. أعد فتح Snapchat وسجل الدخول. هذا يحل المشكلة في معظم الأوقات.", - "notice_step_2": "2. إذا استمر الفشل، اضغط على زر تسجيل الدخول بشكل متكرر. هذا عادة ما يغطي الجزء التالي.", - "notice_step_3": "3. إذا استمر الفشل، امسح بيانات Snapchat، وعطل أي VPN، وسجل الدخول مرة أخرى.", - "notice_rooted_title": "لمستخدمي الروت:", - "notice_rooted_body": "أعد فتح Snapchat وسجل الدخول. إذا استمر الفشل، عطل PurrfectSnap في LSPosed، سجل الدخول، ثم أعد تمكين PurrfectSnap.", - "warnings_info": "حدث {count} تحذير(ات) أثناء إنشاء التعيينات:\n\n{warnings}", - "progress_hint": "هذا يأخذ لحظة فقط. اترك التطبيق مفتوحاً بينما يحدث السحر!" + "dialog": "\u062c\u0627\u0631\u064a \u0625\u0646\u0634\u0627\u0621 \u0627\u0644\u062a\u0639\u064a\u064a\u0646\u0627\u062a (Mappings)...", + "generate_failure_no_snapchat": "\u0644\u0645 \u064a\u062a\u0645\u0643\u0646 PurrfectSnap \u0645\u0646 \u0627\u0643\u062a\u0634\u0627\u0641 Snapchat\u060c \u064a\u0631\u062c\u0649 \u0645\u062d\u0627\u0648\u0644\u0629 \u0625\u0639\u0627\u062f\u0629 \u062a\u062b\u0628\u064a\u062a Snapchat.", + "generate_failure": "\u062d\u062f\u062b \u062e\u0637\u0623 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u0625\u0646\u0634\u0627\u0621 \u0627\u0644\u062a\u0639\u064a\u064a\u0646\u0627\u062a\u060c \u064a\u0631\u062c\u0649 \u0627\u0644\u0645\u062d\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649.", + "confirm_understand_timeout": "\u0623\u0646\u0627 \u0623\u0641\u0647\u0645 ({seconds}\u062b)", + "confirm_understand": "\u0623\u0646\u0627 \u0623\u0641\u0647\u0645", + "notice_title": "\u064a\u0631\u062c\u0649 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0629!", + "notice_intro": "\u0625\u0630\u0627 \u0631\u0623\u064a\u062a \u062e\u0637\u0623 \"\u0627\u0644\u062d\u0633\u0627\u0628 \u0645\u0639\u0637\u0644 \u0645\u0624\u0642\u062a\u0627\u064b\" \u0623\u062b\u0646\u0627\u0621 \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644\u060c \u0644\u0627 \u062a\u0642\u0644\u0642. \u0627\u062a\u0628\u0639 \u0647\u0630\u0647 \u0627\u0644\u062e\u0637\u0648\u0627\u062a \u0628\u0627\u0644\u062a\u0631\u062a\u064a\u0628:", + "notice_step_1": "1. \u0623\u0639\u062f \u0641\u062a\u062d Snapchat \u0648\u0633\u062c\u0644 \u0627\u0644\u062f\u062e\u0648\u0644. \u0647\u0630\u0627 \u064a\u062d\u0644 \u0627\u0644\u0645\u0634\u0643\u0644\u0629 \u0641\u064a \u0645\u0639\u0638\u0645 \u0627\u0644\u0623\u0648\u0642\u0627\u062a.", + "notice_step_2": "2. \u0625\u0630\u0627 \u0627\u0633\u062a\u0645\u0631 \u0627\u0644\u0641\u0634\u0644\u060c \u0627\u0636\u063a\u0637 \u0639\u0644\u0649 \u0632\u0631 \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644 \u0628\u0634\u0643\u0644 \u0645\u062a\u0643\u0631\u0631. \u0647\u0630\u0627 \u0639\u0627\u062f\u0629 \u0645\u0627 \u064a\u063a\u0637\u064a \u0627\u0644\u062c\u0632\u0621 \u0627\u0644\u062a\u0627\u0644\u064a.", + "notice_step_3": "3. \u0625\u0630\u0627 \u0627\u0633\u062a\u0645\u0631 \u0627\u0644\u0641\u0634\u0644\u060c \u0627\u0645\u0633\u062d \u0628\u064a\u0627\u0646\u0627\u062a Snapchat\u060c \u0648\u0639\u0637\u0644 \u0623\u064a VPN\u060c \u0648\u0633\u062c\u0644 \u0627\u0644\u062f\u062e\u0648\u0644 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649.", + "notice_rooted_title": "\u0644\u0645\u0633\u062a\u062e\u062f\u0645\u064a \u0627\u0644\u0631\u0648\u062a:", + "notice_rooted_body": "\u0623\u0639\u062f \u0641\u062a\u062d Snapchat \u0648\u0633\u062c\u0644 \u0627\u0644\u062f\u062e\u0648\u0644. \u0625\u0630\u0627 \u0627\u0633\u062a\u0645\u0631 \u0627\u0644\u0641\u0634\u0644\u060c \u0639\u0637\u0644 PurrfectSnap \u0641\u064a LSPosed\u060c \u0633\u062c\u0644 \u0627\u0644\u062f\u062e\u0648\u0644\u060c \u062b\u0645 \u0623\u0639\u062f \u062a\u0645\u0643\u064a\u0646 PurrfectSnap.", + "warnings_info": "\u062d\u062f\u062b {count} \u062a\u062d\u0630\u064a\u0631(\u0627\u062a) \u0623\u062b\u0646\u0627\u0621 \u0625\u0646\u0634\u0627\u0621 \u0627\u0644\u062a\u0639\u064a\u064a\u0646\u0627\u062a:\n\n{warnings}", + "progress_hint": "\u0647\u0630\u0627 \u064a\u0623\u062e\u0630 \u0644\u062d\u0638\u0629 \u0641\u0642\u0637. \u0627\u062a\u0631\u0643 \u0627\u0644\u062a\u0637\u0628\u064a\u0642 \u0645\u0641\u062a\u0648\u062d\u0627\u064b \u0628\u064a\u0646\u0645\u0627 \u064a\u062d\u062f\u062b \u0627\u0644\u0633\u062d\u0631!" }, "patch": { - "title": "المصحح التلقائي", - "ready_log": "المصحح التلقائي جاهز.", - "install_confirmed_log": "تم تأكيد تثبيت Snapchat. يمكنك المتابعة.", - "download_recommended_status": "جاري تنزيل إصدار Snapchat الموصى به ({version})...", - "starting_log": "بدء المصحح التلقائي لإصدار Snapchat الموصى به.", - "uninstall_prompt_status": "Snapchat مثبت. يرجى إلغاء تثبيته أولاً (لا تحتفظ بالبيانات)، ثم ابدأ المصحح التلقائي مرة أخرى.", - "uninstall_prompt_error": "Snapchat لا يزال مثبتاً. قم بإلغاء تثبيته أولاً للمتابعة.", - "module_apk_not_found_error": "ملف apk للوحدة غير موجود", - "fetching_apk_status": "جاري جلب APK الخاص بـ Snapchat الموصى به...", - "download_failed_error": "فشل التنزيل", - "download_completed_status": "اكتمل التنزيل: {fileName}", - "starting_patch_status": "بدء التصحيح المدعوم بواسطة Jingmatrix Lspatch", - "patched_not_produced_error": "لم يتم إنتاج الـ apk المعدل", - "patched_ready_status": "النسخة المعدلة جاهزة. قم بالتثبيت للإنهاء.", - "failed_status": "فشل: {message}", - "mark_installed_log": "تم تعليمه كمثبت يدوياً. يمكنك المتابعة.", - "issues_title": "تواجه مشاكل؟", - "issues_confirm": "فهمت", - "issues_heading": "كيفية إصلاح أخطاء التثبيت", - "issues_conflict_issue": "المشكلة: لا يمكن تثبيت التطبيق لأنه يتعارض مع حزمة موجودة.", - "issues_conflict_fix": "الحل: قم بتنزيل Snapchat من متجر Play وقم بإلغاء تثبيته دون الاحتفاظ بالبيانات. شغل المصحح التلقائي مرة أخرى. إذا لم ينجح الأمر، شغل الأمر التالي:", + "title": "\u0627\u0644\u0645\u0635\u062d\u062d \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "ready_log": "\u0627\u0644\u0645\u0635\u062d\u062d \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u062c\u0627\u0647\u0632.", + "install_confirmed_log": "\u062a\u0645 \u062a\u0623\u0643\u064a\u062f \u062a\u062b\u0628\u064a\u062a Snapchat. \u064a\u0645\u0643\u0646\u0643 \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629.", + "download_recommended_status": "\u062c\u0627\u0631\u064a \u062a\u0646\u0632\u064a\u0644 \u0625\u0635\u062f\u0627\u0631 Snapchat \u0627\u0644\u0645\u0648\u0635\u0649 \u0628\u0647 ({version})...", + "starting_log": "\u0628\u062f\u0621 \u0627\u0644\u0645\u0635\u062d\u062d \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0625\u0635\u062f\u0627\u0631 Snapchat \u0627\u0644\u0645\u0648\u0635\u0649 \u0628\u0647.", + "uninstall_prompt_status": "Snapchat \u0645\u062b\u0628\u062a. \u064a\u0631\u062c\u0649 \u0625\u0644\u063a\u0627\u0621 \u062a\u062b\u0628\u064a\u062a\u0647 \u0623\u0648\u0644\u0627\u064b (\u0644\u0627 \u062a\u062d\u062a\u0641\u0638 \u0628\u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a)\u060c \u062b\u0645 \u0627\u0628\u062f\u0623 \u0627\u0644\u0645\u0635\u062d\u062d \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0645\u0631\u0629 \u0623\u062e\u0631\u0649.", + "uninstall_prompt_error": "Snapchat \u0644\u0627 \u064a\u0632\u0627\u0644 \u0645\u062b\u0628\u062a\u0627\u064b. \u0642\u0645 \u0628\u0625\u0644\u063a\u0627\u0621 \u062a\u062b\u0628\u064a\u062a\u0647 \u0623\u0648\u0644\u0627\u064b \u0644\u0644\u0645\u062a\u0627\u0628\u0639\u0629.", + "module_apk_not_found_error": "\u0645\u0644\u0641 apk \u0644\u0644\u0648\u062d\u062f\u0629 \u063a\u064a\u0631 \u0645\u0648\u062c\u0648\u062f", + "fetching_apk_status": "\u062c\u0627\u0631\u064a \u062c\u0644\u0628 APK \u0627\u0644\u062e\u0627\u0635 \u0628\u0640 Snapchat \u0627\u0644\u0645\u0648\u0635\u0649 \u0628\u0647...", + "download_failed_error": "\u0641\u0634\u0644 \u0627\u0644\u062a\u0646\u0632\u064a\u0644", + "download_completed_status": "\u0627\u0643\u062a\u0645\u0644 \u0627\u0644\u062a\u0646\u0632\u064a\u0644: {fileName}", + "starting_patch_status": "\u0628\u062f\u0621 \u0627\u0644\u062a\u0635\u062d\u064a\u062d \u0627\u0644\u0645\u062f\u0639\u0648\u0645 \u0628\u0648\u0627\u0633\u0637\u0629 Jingmatrix Lspatch", + "patched_not_produced_error": "\u0644\u0645 \u064a\u062a\u0645 \u0625\u0646\u062a\u0627\u062c \u0627\u0644\u0640 apk \u0627\u0644\u0645\u0639\u062f\u0644", + "patched_ready_status": "\u0627\u0644\u0646\u0633\u062e\u0629 \u0627\u0644\u0645\u0639\u062f\u0644\u0629 \u062c\u0627\u0647\u0632\u0629. \u0642\u0645 \u0628\u0627\u0644\u062a\u062b\u0628\u064a\u062a \u0644\u0644\u0625\u0646\u0647\u0627\u0621.", + "failed_status": "\u0641\u0634\u0644: {message}", + "mark_installed_log": "\u062a\u0645 \u062a\u0639\u0644\u064a\u0645\u0647 \u0643\u0645\u062b\u0628\u062a \u064a\u062f\u0648\u064a\u0627\u064b. \u064a\u0645\u0643\u0646\u0643 \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629.", + "issues_title": "\u062a\u0648\u0627\u062c\u0647 \u0645\u0634\u0627\u0643\u0644\u061f", + "issues_confirm": "\u0641\u0647\u0645\u062a", + "issues_heading": "\u0643\u064a\u0641\u064a\u0629 \u0625\u0635\u0644\u0627\u062d \u0623\u062e\u0637\u0627\u0621 \u0627\u0644\u062a\u062b\u0628\u064a\u062a", + "issues_conflict_issue": "\u0627\u0644\u0645\u0634\u0643\u0644\u0629: \u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u062b\u0628\u064a\u062a \u0627\u0644\u062a\u0637\u0628\u064a\u0642 \u0644\u0623\u0646\u0647 \u064a\u062a\u0639\u0627\u0631\u0636 \u0645\u0639 \u062d\u0632\u0645\u0629 \u0645\u0648\u062c\u0648\u062f\u0629.", + "issues_conflict_fix": "\u0627\u0644\u062d\u0644: \u0642\u0645 \u0628\u062a\u0646\u0632\u064a\u0644 Snapchat \u0645\u0646 \u0645\u062a\u062c\u0631 Play \u0648\u0642\u0645 \u0628\u0625\u0644\u063a\u0627\u0621 \u062a\u062b\u0628\u064a\u062a\u0647 \u062f\u0648\u0646 \u0627\u0644\u0627\u062d\u062a\u0641\u0627\u0638 \u0628\u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a. \u0634\u063a\u0644 \u0627\u0644\u0645\u0635\u062d\u062d \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0645\u0631\u0629 \u0623\u062e\u0631\u0649. \u0625\u0630\u0627 \u0644\u0645 \u064a\u0646\u062c\u062d \u0627\u0644\u0623\u0645\u0631\u060c \u0634\u063a\u0644 \u0627\u0644\u0623\u0645\u0631 \u0627\u0644\u062a\u0627\u0644\u064a:", "issues_adb_command": "adb uninstall com.snapchat.android", - "issues_invalid_issue": "المشكلة: التطبيق غير مثبت لأن الحزمة تبدو غير صالحة.", - "issues_invalid_fix": "الحل: قم بتنزيل وتثبيت JingMatrix LSPatch، ثم قم بتصحيح نسخة Snapchat (أي نسخة) من هذا النطاق، أي بين 13.65.1.0 و 13.71.0.51، في وضع Integrated. اختر Embed Modules وقم بتضمين PurrfectSnap APK. ثم اختر 'تخطي الإعداد التلقائي' أثناء إعداد PurrfectSnap لتجاوز المصحح التلقائي.", - "status_downloading": "جاري تنزيل Snapchat {percent}%", - "status_patching": "جاري التصحيح...", - "status_initializing": "جاري التهيئة...", - "logs_copied": "تم نسخ السجلات إلى الحافظة.", - "install_success": "تم تثبيت APK المعدل", - "start_button": "بدء التصحيح التلقائي", - "install_button": "تثبيت Snapchat المعدل", - "already_installed_button": "مثبت بالفعل؟", - "powered_by_label": "مدعوم بواسطة Jingmatrix Lspatch", - "logs_title": "السجلات", - "copy_button": "نسخ", + "issues_invalid_issue": "\u0627\u0644\u0645\u0634\u0643\u0644\u0629: \u0627\u0644\u062a\u0637\u0628\u064a\u0642 \u063a\u064a\u0631 \u0645\u062b\u0628\u062a \u0644\u0623\u0646 \u0627\u0644\u062d\u0632\u0645\u0629 \u062a\u0628\u062f\u0648 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d\u0629.", + "issues_invalid_fix": "\u0627\u0644\u062d\u0644: \u0642\u0645 \u0628\u062a\u0646\u0632\u064a\u0644 \u0648\u062a\u062b\u0628\u064a\u062a JingMatrix LSPatch\u060c \u062b\u0645 \u0642\u0645 \u0628\u062a\u0635\u062d\u064a\u062d \u0646\u0633\u062e\u0629 Snapchat (\u0623\u064a \u0646\u0633\u062e\u0629) \u0645\u0646 \u0647\u0630\u0627 \u0627\u0644\u0646\u0637\u0627\u0642\u060c \u0623\u064a \u0628\u064a\u0646 13.65.1.0 \u0648 13.71.0.51\u060c \u0641\u064a \u0648\u0636\u0639 Integrated. \u0627\u062e\u062a\u0631 Embed Modules \u0648\u0642\u0645 \u0628\u062a\u0636\u0645\u064a\u0646 PurrfectSnap APK. \u062b\u0645 \u0627\u062e\u062a\u0631 '\u062a\u062e\u0637\u064a \u0627\u0644\u0625\u0639\u062f\u0627\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a' \u0623\u062b\u0646\u0627\u0621 \u0625\u0639\u062f\u0627\u062f PurrfectSnap \u0644\u062a\u062c\u0627\u0648\u0632 \u0627\u0644\u0645\u0635\u062d\u062d \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a.", + "status_downloading": "\u062c\u0627\u0631\u064a \u062a\u0646\u0632\u064a\u0644 Snapchat {percent}%", + "status_patching": "\u062c\u0627\u0631\u064a \u0627\u0644\u062a\u0635\u062d\u064a\u062d...", + "status_initializing": "\u062c\u0627\u0631\u064a \u0627\u0644\u062a\u0647\u064a\u0626\u0629...", + "logs_copied": "\u062a\u0645 \u0646\u0633\u062e \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0625\u0644\u0649 \u0627\u0644\u062d\u0627\u0641\u0638\u0629.", + "install_success": "\u062a\u0645 \u062a\u062b\u0628\u064a\u062a APK \u0627\u0644\u0645\u0639\u062f\u0644", + "start_button": "\u0628\u062f\u0621 \u0627\u0644\u062a\u0635\u062d\u064a\u062d \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "install_button": "\u062a\u062b\u0628\u064a\u062a Snapchat \u0627\u0644\u0645\u0639\u062f\u0644", + "already_installed_button": "\u0645\u062b\u0628\u062a \u0628\u0627\u0644\u0641\u0639\u0644\u061f", + "powered_by_label": "\u0645\u062f\u0639\u0648\u0645 \u0628\u0648\u0627\u0633\u0637\u0629 Jingmatrix Lspatch", + "logs_title": "\u0627\u0644\u0633\u062c\u0644\u0627\u062a", + "copy_button": "\u0646\u0633\u062e", "log_line_prefix": "- {line}" }, "permissions": { - "dialog": "أكمل هذه الأساسيات للمتابعة:", - "notification_access": "الوصول إلى الإشعارات", - "battery_optimization": "تحسين البطارية", - "display_over_other_apps": "العرض فوق التطبيقات الأخرى", - "request_button": "طلب", - "notification_access_description": "ينبهك بمجرد انتهاء التنزيلات.", - "battery_optimization_description": "يحافظ على المهام الخلفية نشطة دون أن يتم قتلها.", - "display_over_other_apps_description": "يمكن التراكبات العائمة أثناء تواجدك في Snapchat.", - "granted_label": "تم المنح" + "dialog": "\u0623\u0643\u0645\u0644 \u0647\u0630\u0647 \u0627\u0644\u0623\u0633\u0627\u0633\u064a\u0627\u062a \u0644\u0644\u0645\u062a\u0627\u0628\u0639\u0629:", + "notification_access": "\u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0627\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062a", + "battery_optimization": "\u062a\u062d\u0633\u064a\u0646 \u0627\u0644\u0628\u0637\u0627\u0631\u064a\u0629", + "display_over_other_apps": "\u0627\u0644\u0639\u0631\u0636 \u0641\u0648\u0642 \u0627\u0644\u062a\u0637\u0628\u064a\u0642\u0627\u062a \u0627\u0644\u0623\u062e\u0631\u0649", + "request_button": "\u0637\u0644\u0628", + "notification_access_description": "\u064a\u0646\u0628\u0647\u0643 \u0628\u0645\u062c\u0631\u062f \u0627\u0646\u062a\u0647\u0627\u0621 \u0627\u0644\u062a\u0646\u0632\u064a\u0644\u0627\u062a.", + "battery_optimization_description": "\u064a\u062d\u0627\u0641\u0638 \u0639\u0644\u0649 \u0627\u0644\u0645\u0647\u0627\u0645 \u0627\u0644\u062e\u0644\u0641\u064a\u0629 \u0646\u0634\u0637\u0629 \u062f\u0648\u0646 \u0623\u0646 \u064a\u062a\u0645 \u0642\u062a\u0644\u0647\u0627.", + "display_over_other_apps_description": "\u064a\u0645\u0643\u0646 \u0627\u0644\u062a\u0631\u0627\u0643\u0628\u0627\u062a \u0627\u0644\u0639\u0627\u0626\u0645\u0629 \u0623\u062b\u0646\u0627\u0621 \u062a\u0648\u0627\u062c\u062f\u0643 \u0641\u064a Snapchat.", + "granted_label": "\u062a\u0645 \u0627\u0644\u0645\u0646\u062d" }, "pick_language": { - "current_selection": "الاختيار الحالي", - "browse_languages": "تصفح اللغات", - "change_anytime_hint": "يمكنك تغيير هذا في أي وقت من إعدادات PurrfectSnap.", - "available_languages": "اللغات المتاحة" + "current_selection": "\u0627\u0644\u0627\u062e\u062a\u064a\u0627\u0631 \u0627\u0644\u062d\u0627\u0644\u064a", + "browse_languages": "\u062a\u0635\u0641\u062d \u0627\u0644\u0644\u063a\u0627\u062a", + "change_anytime_hint": "\u064a\u0645\u0643\u0646\u0643 \u062a\u063a\u064a\u064a\u0631 \u0647\u0630\u0627 \u0641\u064a \u0623\u064a \u0648\u0642\u062a \u0645\u0646 \u0625\u0639\u062f\u0627\u062f\u0627\u062a PurrfectSnap.", + "available_languages": "\u0627\u0644\u0644\u063a\u0627\u062a \u0627\u0644\u0645\u062a\u0627\u062d\u0629" }, "root_install": { - "title": "مثبت Snapchat", - "ready_log": "مثبت Snapchat جاهز.", - "install_confirmed_log": "تم تأكيد تثبيت Snapchat. يمكنك المتابعة.", - "download_recommended_status": "جاري تنزيل إصدار Snapchat الموصى به ({version})...", - "mark_installed_log": "تم تعليمه كمثبت يدوياً. يمكنك المتابعة.", - "start_download_log": "بدء تنزيل Snapchat للتثبيت بصلاحيات الروت.", - "uninstall_prompt_status": "Snapchat مثبت. يرجى إلغاء تثبيته أولاً (لا تحتفظ بالبيانات)، ثم حاول مرة أخرى.", - "uninstall_prompt_error": "Snapchat لا يزال مثبتاً. قم بإلغاء تثبيته أولاً للمتابعة.", - "fetching_apk_status": "جاري جلب APK الخاص بـ Snapchat الموصى به...", - "download_failed_error": "فشل التنزيل", - "download_completed_status": "اكتمل التنزيل: {fileName}", - "launching_installer_status": "تشغيل المثبت...", - "failed_status": "فشل: {message}", - "status_downloading": "جاري تنزيل Snapchat {percent}%", - "status_preparing": "جاري تحضير المثبت...", - "logs_copied": "تم نسخ السجلات إلى الحافظة.", - "install_success": "تم تثبيت Snapchat", - "download_button": "تنزيل Snapchat", - "install_button": "تثبيت Snapchat", - "already_installed_button": "مثبت بالفعل؟", - "logs_title": "السجلات", - "copy_button": "نسخ", + "title": "\u0645\u062b\u0628\u062a Snapchat", + "ready_log": "\u0645\u062b\u0628\u062a Snapchat \u062c\u0627\u0647\u0632.", + "install_confirmed_log": "\u062a\u0645 \u062a\u0623\u0643\u064a\u062f \u062a\u062b\u0628\u064a\u062a Snapchat. \u064a\u0645\u0643\u0646\u0643 \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629.", + "download_recommended_status": "\u062c\u0627\u0631\u064a \u062a\u0646\u0632\u064a\u0644 \u0625\u0635\u062f\u0627\u0631 Snapchat \u0627\u0644\u0645\u0648\u0635\u0649 \u0628\u0647 ({version})...", + "mark_installed_log": "\u062a\u0645 \u062a\u0639\u0644\u064a\u0645\u0647 \u0643\u0645\u062b\u0628\u062a \u064a\u062f\u0648\u064a\u0627\u064b. \u064a\u0645\u0643\u0646\u0643 \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629.", + "start_download_log": "\u0628\u062f\u0621 \u062a\u0646\u0632\u064a\u0644 Snapchat \u0644\u0644\u062a\u062b\u0628\u064a\u062a \u0628\u0635\u0644\u0627\u062d\u064a\u0627\u062a \u0627\u0644\u0631\u0648\u062a.", + "uninstall_prompt_status": "Snapchat \u0645\u062b\u0628\u062a. \u064a\u0631\u062c\u0649 \u0625\u0644\u063a\u0627\u0621 \u062a\u062b\u0628\u064a\u062a\u0647 \u0623\u0648\u0644\u0627\u064b (\u0644\u0627 \u062a\u062d\u062a\u0641\u0638 \u0628\u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a)\u060c \u062b\u0645 \u062d\u0627\u0648\u0644 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649.", + "uninstall_prompt_error": "Snapchat \u0644\u0627 \u064a\u0632\u0627\u0644 \u0645\u062b\u0628\u062a\u0627\u064b. \u0642\u0645 \u0628\u0625\u0644\u063a\u0627\u0621 \u062a\u062b\u0628\u064a\u062a\u0647 \u0623\u0648\u0644\u0627\u064b \u0644\u0644\u0645\u062a\u0627\u0628\u0639\u0629.", + "fetching_apk_status": "\u062c\u0627\u0631\u064a \u062c\u0644\u0628 APK \u0627\u0644\u062e\u0627\u0635 \u0628\u0640 Snapchat \u0627\u0644\u0645\u0648\u0635\u0649 \u0628\u0647...", + "download_failed_error": "\u0641\u0634\u0644 \u0627\u0644\u062a\u0646\u0632\u064a\u0644", + "download_completed_status": "\u0627\u0643\u062a\u0645\u0644 \u0627\u0644\u062a\u0646\u0632\u064a\u0644: {fileName}", + "launching_installer_status": "\u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0645\u062b\u0628\u062a...", + "failed_status": "\u0641\u0634\u0644: {message}", + "status_downloading": "\u062c\u0627\u0631\u064a \u062a\u0646\u0632\u064a\u0644 Snapchat {percent}%", + "status_preparing": "\u062c\u0627\u0631\u064a \u062a\u062d\u0636\u064a\u0631 \u0627\u0644\u0645\u062b\u0628\u062a...", + "logs_copied": "\u062a\u0645 \u0646\u0633\u062e \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0625\u0644\u0649 \u0627\u0644\u062d\u0627\u0641\u0638\u0629.", + "install_success": "\u062a\u0645 \u062a\u062b\u0628\u064a\u062a Snapchat", + "download_button": "\u062a\u0646\u0632\u064a\u0644 Snapchat", + "install_button": "\u062a\u062b\u0628\u064a\u062a Snapchat", + "already_installed_button": "\u0645\u062b\u0628\u062a \u0628\u0627\u0644\u0641\u0639\u0644\u061f", + "logs_title": "\u0627\u0644\u0633\u062c\u0644\u0627\u062a", + "copy_button": "\u0646\u0633\u062e", "log_line_prefix": "- {line}" }, "save_folder": { - "description": "يرجى اختيار الموقع الذي يجب تنزيل الوسائط إليه.", - "destination_label": "الوجهة", - "system_default_label": "الافتراضي للنظام", - "use_default_location_button": "استخدام الموقع الافتراضي", - "no_picker_title": "منتقي المجلدات غير متاح", - "no_picker_message": "بعض بيئات التطبيقات المنسوخة/المزدوجة تحظر منتقي مجلدات النظام. يمكنك الاستمرار باستخدام موقع الحفظ الافتراضي للنظام، أو فتح التطبيق خارج وضع الاستنساخ لاختيار مجلد مخصص.", - "use_default_button": "استخدام الافتراضي", - "permission_hint": "يتطلب PurrfectSnap أذونات التخزين لتنزيل وحفظ الوسائط من Snapchat." + "description": "\u064a\u0631\u062c\u0649 \u0627\u062e\u062a\u064a\u0627\u0631 \u0627\u0644\u0645\u0648\u0642\u0639 \u0627\u0644\u0630\u064a \u064a\u062c\u0628 \u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0625\u0644\u064a\u0647.", + "destination_label": "\u0627\u0644\u0648\u062c\u0647\u0629", + "system_default_label": "\u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a \u0644\u0644\u0646\u0638\u0627\u0645", + "use_default_location_button": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u0645\u0648\u0642\u0639 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a", + "no_picker_title": "\u0645\u0646\u062a\u0642\u064a \u0627\u0644\u0645\u062c\u0644\u062f\u0627\u062a \u063a\u064a\u0631 \u0645\u062a\u0627\u062d", + "no_picker_message": "\u0628\u0639\u0636 \u0628\u064a\u0626\u0627\u062a \u0627\u0644\u062a\u0637\u0628\u064a\u0642\u0627\u062a \u0627\u0644\u0645\u0646\u0633\u0648\u062e\u0629/\u0627\u0644\u0645\u0632\u062f\u0648\u062c\u0629 \u062a\u062d\u0638\u0631 \u0645\u0646\u062a\u0642\u064a \u0645\u062c\u0644\u062f\u0627\u062a \u0627\u0644\u0646\u0638\u0627\u0645. \u064a\u0645\u0643\u0646\u0643 \u0627\u0644\u0627\u0633\u062a\u0645\u0631\u0627\u0631 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0645\u0648\u0642\u0639 \u0627\u0644\u062d\u0641\u0638 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a \u0644\u0644\u0646\u0638\u0627\u0645\u060c \u0623\u0648 \u0641\u062a\u062d \u0627\u0644\u062a\u0637\u0628\u064a\u0642 \u062e\u0627\u0631\u062c \u0648\u0636\u0639 \u0627\u0644\u0627\u0633\u062a\u0646\u0633\u0627\u062e \u0644\u0627\u062e\u062a\u064a\u0627\u0631 \u0645\u062c\u0644\u062f \u0645\u062e\u0635\u0635.", + "use_default_button": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a", + "permission_hint": "\u064a\u062a\u0637\u0644\u0628 PurrfectSnap \u0623\u0630\u0648\u0646\u0627\u062a \u0627\u0644\u062a\u062e\u0632\u064a\u0646 \u0644\u062a\u0646\u0632\u064a\u0644 \u0648\u062d\u0641\u0638 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0645\u0646 Snapchat." } }, "scopes": { - "friend": "صديق", - "group": "مجموعة" + "friend": "\u0635\u062f\u064a\u0642", + "group": "\u0645\u062c\u0645\u0648\u0639\u0629" }, "manager": { "routes": { - "tasks": "المهام", - "features": "المميزات", - "manage_rule_feature": "إدارة ميزة القاعدة", - "home": "الرئيسية", - "home_about": "حول", - "home_settings": "الإعدادات", - "home_logs": "السجلات", - "logger_history": "سجل المسجل", - "logged_stories": "القصص المسجلة", - "friend_tracker": "متتبع الأصدقاء", - "friend_tracker_catalog": "كتالوج متتبع الأصدقاء", - "manage_friend_tracker_repos": "إدارة مستودعات متتبع الأصدقاء", - "edit_rule": "تعديل القاعدة", - "file_imports": "استيراد الملفات", - "manage_repos": "إدارة المستودعات", - "social": "اجتماعي", - "manage_scope": "إدارة النطاق", - "messaging_preview": "معاينة", - "scripts": "السكربتات", - "manage_script_repos": "إدارة مستودعات السكربتات", - "view_logger_history": "عرض سجل المسجل", - "better_location": "موقع أفضل" + "tasks": "\u0627\u0644\u0645\u0647\u0627\u0645", + "features": "\u0627\u0644\u0645\u0645\u064a\u0632\u0627\u062a", + "manage_rule_feature": "\u0625\u062f\u0627\u0631\u0629 \u0645\u064a\u0632\u0629 \u0627\u0644\u0642\u0627\u0639\u062f\u0629", + "home": "\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629", + "home_about": "\u062d\u0648\u0644", + "home_settings": "\u0627\u0644\u0625\u0639\u062f\u0627\u062f\u0627\u062a", + "home_logs": "\u0627\u0644\u0633\u062c\u0644\u0627\u062a", + "logger_history": "\u0633\u062c\u0644 \u0627\u0644\u0645\u0633\u062c\u0644", + "logged_stories": "\u0627\u0644\u0642\u0635\u0635 \u0627\u0644\u0645\u0633\u062c\u0644\u0629", + "friend_tracker": "\u0645\u062a\u062a\u0628\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "friend_tracker_catalog": "\u0643\u062a\u0627\u0644\u0648\u062c \u0645\u062a\u062a\u0628\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "manage_friend_tracker_repos": "\u0625\u062f\u0627\u0631\u0629 \u0645\u0633\u062a\u0648\u062f\u0639\u0627\u062a \u0645\u062a\u062a\u0628\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "edit_rule": "\u062a\u0639\u062f\u064a\u0644 \u0627\u0644\u0642\u0627\u0639\u062f\u0629", + "file_imports": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0645\u0644\u0641\u0627\u062a", + "manage_repos": "\u0625\u062f\u0627\u0631\u0629 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639\u0627\u062a", + "social": "\u0627\u062c\u062a\u0645\u0627\u0639\u064a", + "manage_scope": "\u0625\u062f\u0627\u0631\u0629 \u0627\u0644\u0646\u0637\u0627\u0642", + "messaging_preview": "\u0645\u0639\u0627\u064a\u0646\u0629", + "scripts": "\u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a", + "manage_script_repos": "\u0625\u062f\u0627\u0631\u0629 \u0645\u0633\u062a\u0648\u062f\u0639\u0627\u062a \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a", + "view_logger_history": "\u0639\u0631\u0636 \u0633\u062c\u0644 \u0627\u0644\u0645\u0633\u062c\u0644", + "better_location": "\u0645\u0648\u0642\u0639 \u0623\u0641\u0636\u0644" }, "navigation": { - "customize_bottom_bar_title": "تخصيص الشريط السفلي", - "customize_bottom_bar_subtitle": "اختر علامات التبويب التي تظهر على شاشتك الرئيسية", - "available_tabs_title": "علامات التبويب المتاحة", - "shown_tabs_title": "علامات التبويب المعروضة", - "reset_button": "إعادة تعيين", - "done_button": "تم" + "customize_bottom_bar_title": "\u062a\u062e\u0635\u064a\u0635 \u0627\u0644\u0634\u0631\u064a\u0637 \u0627\u0644\u0633\u0641\u0644\u064a", + "customize_bottom_bar_subtitle": "\u0627\u062e\u062a\u0631 \u0639\u0644\u0627\u0645\u0627\u062a \u0627\u0644\u062a\u0628\u0648\u064a\u0628 \u0627\u0644\u062a\u064a \u062a\u0638\u0647\u0631 \u0639\u0644\u0649 \u0634\u0627\u0634\u062a\u0643 \u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629", + "available_tabs_title": "\u0639\u0644\u0627\u0645\u0627\u062a \u0627\u0644\u062a\u0628\u0648\u064a\u0628 \u0627\u0644\u0645\u062a\u0627\u062d\u0629", + "shown_tabs_title": "\u0639\u0644\u0627\u0645\u0627\u062a \u0627\u0644\u062a\u0628\u0648\u064a\u0628 \u0627\u0644\u0645\u0639\u0631\u0648\u0636\u0629", + "reset_button": "\u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646", + "done_button": "\u062a\u0645" }, "sections": { "home": { - "version_title": "v{versionName} \u00b7 بواسطة ΞTΞRNAL", - "update_title": "تحديث PurrfectSnap", - "update_content": "الإصدار {version} متاح!", - "update_button": "تنزيل", - "hero_tagline": "وحدة Xposed تهدف لتحسين تجربة Snapchat الخاصة بك", - "hero_version_label": "الإصدار: {version} - {channel}", - "hero_build_label": "البناء: {build}", - "update_ready_label": "جاهز للتثبيت", - "purr_aura_active_label": "PurrAura نشط!", - "purr_aura_inactive_label": "PurrAura غير نشط", - "open_settings_button": "فتح الإعدادات", - "wiki_button": "ويكي", + "version_title": "v{versionName} \u00b7 \u0628\u0648\u0627\u0633\u0637\u0629 \u039eT\u039eRNAL", + "update_title": "\u062a\u062d\u062f\u064a\u062b PurrfectSnap", + "update_content": "\u0627\u0644\u0625\u0635\u062f\u0627\u0631 {version} \u0645\u062a\u0627\u062d!", + "update_button": "\u062a\u0646\u0632\u064a\u0644", + "hero_tagline": "\u0648\u062d\u062f\u0629 Xposed \u062a\u0647\u062f\u0641 \u0644\u062a\u062d\u0633\u064a\u0646 \u062a\u062c\u0631\u0628\u0629 Snapchat \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0643", + "hero_version_label": "\u0627\u0644\u0625\u0635\u062f\u0627\u0631: {version}", + "hero_build_label": "\u0627\u0644\u0628\u0646\u0627\u0621: {build}", + "update_ready_label": "\u062c\u0627\u0647\u0632 \u0644\u0644\u062a\u062b\u0628\u064a\u062a", + "purr_aura_active_label": "PurrAura \u0646\u0634\u0637!", + "purr_aura_inactive_label": "PurrAura \u063a\u064a\u0631 \u0646\u0634\u0637", + "open_settings_button": "\u0641\u062a\u062d \u0627\u0644\u0625\u0639\u062f\u0627\u062f\u0627\u062a", + "wiki_button": "\u0648\u064a\u0643\u064a", "github_button": "GitHub", - "telegram_button": "تيليجرام", - "channel_label_stable": "مستقر", - "channel_label_prerelease": "ما قبل الإصدار", - "announcements_button_description": "الإعلانات", - "update_arch_not_supported_toast": "معمارية جهازك غير مدعومة للتحديثات التلقائية.", - "update_download_started_toast": "بدأ التنزيل", - "update_download_completed_toast": "اكتمل التنزيل", - "update_install_failed_toast": "فشل تثبيت التحديث. تحقق من السجلات لمزيد من التفاصيل.", - "update_download_failed_toast": "فشل التنزيل: {error}", - "debug_build_summary_title": "أنت تقوم بتشغيل نسخة تصحيح (Debug) من PurrfectSnap", - "debug_build_summary_content": "الإصدار {versionName} ({versionCode})", - "debug_build_summary_date": "تاريخ البناء: {date} (منذ {days} أيام)", - "quick_actions_title": "إجراءات سريعة", - "quick_actions_empty_title": "لا توجد مربعات سريعة بعد", - "quick_actions_empty_subtitle": "صمم شبكة أحلامك بالإجراءات التي تستخدمها أكثر.", - "quick_actions_add_tile_button": "إضافة مربع", - "quick_actions_manage_button": "إدارة", - "quick_actions_count_label": "{count} اختصارات منتقاة", - "enabled": "مفعل", - "disabled": "معطل", - "changelog_dialog_title": "سجل التغييرات", - "changelog_dialog_update_button": "تحديث", - "changelog_dialog_cancel_button": "إلغاء", - "changelog_dialog_loading": "جاري تحميل سجل التغييرات...", - "changelog_dialog_error": "فشل تحميل سجل التغييرات", - "changelog_dialog_empty": "سجل التغييرات غير متاح", - "announcements_dialog_title": "الإعلانات", - "announcements_dialog_close_button": "إغلاق", - "announcements_dialog_loading": "جاري تحميل الإعلانات...", - "announcements_dialog_error": "فشل تحميل الإعلانات", - "announcements_dialog_empty": "الإعلانات غير متاحة" + "telegram_button": "\u062a\u064a\u0644\u064a\u062c\u0631\u0627\u0645", + "channel_label_stable": "\u0645\u0633\u062a\u0642\u0631", + "channel_label_prerelease": "\u0645\u0627 \u0642\u0628\u0644 \u0627\u0644\u0625\u0635\u062f\u0627\u0631", + "announcements_button_description": "\u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a", + "update_arch_not_supported_toast": "\u0645\u0639\u0645\u0627\u0631\u064a\u0629 \u062c\u0647\u0627\u0632\u0643 \u063a\u064a\u0631 \u0645\u062f\u0639\u0648\u0645\u0629 \u0644\u0644\u062a\u062d\u062f\u064a\u062b\u0627\u062a \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a\u0629.", + "update_download_started_toast": "\u0628\u062f\u0623 \u0627\u0644\u062a\u0646\u0632\u064a\u0644", + "update_download_completed_toast": "\u0627\u0643\u062a\u0645\u0644 \u0627\u0644\u062a\u0646\u0632\u064a\u0644", + "update_install_failed_toast": "\u0641\u0634\u0644 \u062a\u062b\u0628\u064a\u062a \u0627\u0644\u062a\u062d\u062f\u064a\u062b. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644.", + "update_download_failed_toast": "\u0641\u0634\u0644 \u0627\u0644\u062a\u0646\u0632\u064a\u0644: {error}", + "debug_build_summary_title": "\u0623\u0646\u062a \u062a\u0642\u0648\u0645 \u0628\u062a\u0634\u063a\u064a\u0644 \u0646\u0633\u062e\u0629 \u062a\u0635\u062d\u064a\u062d (Debug) \u0645\u0646 PurrfectSnap", + "debug_build_summary_content": "\u0627\u0644\u0625\u0635\u062f\u0627\u0631 {versionName} ({versionCode})", + "debug_build_summary_date": "\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0628\u0646\u0627\u0621: {date} (\u0645\u0646\u0630 {days} \u0623\u064a\u0627\u0645)", + "quick_actions_title": "\u0625\u062c\u0631\u0627\u0621\u0627\u062a \u0633\u0631\u064a\u0639\u0629", + "quick_actions_empty_title": "\u0644\u0627 \u062a\u0648\u062c\u062f \u0645\u0631\u0628\u0639\u0627\u062a \u0633\u0631\u064a\u0639\u0629 \u0628\u0639\u062f", + "quick_actions_empty_subtitle": "\u0635\u0645\u0645 \u0634\u0628\u0643\u0629 \u0623\u062d\u0644\u0627\u0645\u0643 \u0628\u0627\u0644\u0625\u062c\u0631\u0627\u0621\u0627\u062a \u0627\u0644\u062a\u064a \u062a\u0633\u062a\u062e\u062f\u0645\u0647\u0627 \u0623\u0643\u062b\u0631.", + "quick_actions_add_tile_button": "\u0625\u0636\u0627\u0641\u0629 \u0645\u0631\u0628\u0639", + "quick_actions_manage_button": "\u0625\u062f\u0627\u0631\u0629", + "quick_actions_count_label": "{count} \u0627\u062e\u062a\u0635\u0627\u0631\u0627\u062a \u0645\u0646\u062a\u0642\u0627\u0629", + "enabled": "\u0645\u0641\u0639\u0644", + "disabled": "\u0645\u0639\u0637\u0644", + "changelog_dialog_title": "\u0633\u062c\u0644 \u0627\u0644\u062a\u063a\u064a\u064a\u0631\u0627\u062a", + "changelog_dialog_update_button": "\u062a\u062d\u062f\u064a\u062b", + "changelog_dialog_cancel_button": "\u0625\u0644\u063a\u0627\u0621", + "changelog_dialog_loading": "\u062c\u0627\u0631\u064a \u062a\u062d\u0645\u064a\u0644 \u0633\u062c\u0644 \u0627\u0644\u062a\u063a\u064a\u064a\u0631\u0627\u062a...", + "changelog_dialog_error": "\u0641\u0634\u0644 \u062a\u062d\u0645\u064a\u0644 \u0633\u062c\u0644 \u0627\u0644\u062a\u063a\u064a\u064a\u0631\u0627\u062a", + "changelog_dialog_empty": "\u0633\u062c\u0644 \u0627\u0644\u062a\u063a\u064a\u064a\u0631\u0627\u062a \u063a\u064a\u0631 \u0645\u062a\u0627\u062d", + "announcements_dialog_title": "\u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a", + "announcements_dialog_close_button": "\u0625\u063a\u0644\u0627\u0642", + "announcements_dialog_loading": "\u062c\u0627\u0631\u064a \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a...", + "announcements_dialog_error": "\u0641\u0634\u0644 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a", + "announcements_dialog_empty": "\u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a \u063a\u064a\u0631 \u0645\u062a\u0627\u062d\u0629" }, "home_about": { "about_title": "PurrfectSnap", - "about_tagline": "وحدة Xposed تهدف لتحسين تجربة Snapchat الخاصة بك!", - "about_lead_developers_title": "المطورون الرئيسيون", - "about_story_title": "قصتنا", - "about_story": "تأسست PurrfectSnap في 2 أكتوبر 2025، كتفرع من SnapEnhance بواسطة ΞTΞRNAL مع رؤية لتقديم تجربة Snapchat عالية الجودة التي يستحقها المستخدمون. كان من المفترض أن يكون هذا التطبيق مجرد تحديث بسيط في مستودع SnapEnhance، لكنه سرعان ما أصبح تطبيقاً منفصلاً حيث استمر المساهمون في إضافة الميزات. ثم انضم المطور إلى الفريق، وسرعان ما حقق هذا التطبيق نجاحاً هائلاً. تلقينا الكثير من الحب والدعم وحصلنا على أكثر من 1000 تنزيل في يومين فقط! نشكر جميع المستخدمين والمساهمين؛ بدون دعمكم، لم نكن لنصل إلى هذا المكان. نود أيضاً أن نعرب عن شكرنا الجزيل لـ rhunk، المطور الرئيسي لـ SnapEnhance، فبدونه لم يكن هذا التطبيق موجوداً حتى. نحن ممتنون له للغاية. أخيراً، نود أن نشكر جميع مسؤولينا، ولا سيما: CLASSIC GENIUS و Harry و Sujal و Zain و schrodingerspet، الذين كانوا معنا منذ البداية. نود أيضاً أن نشكر جميع المختبرين، ولا سيما Leo و Toxic، الذين اختبروا وأبلغوا عن الأخطاء باستمرار. نحن ممتنون للغاية لمساهمتكم.", - "about_thanks_title": "مع الحب، فريق PurrfectSnap", - "about_magic_toast": "اضغط 5 مرات في هذه الشاشة لرؤية بعض السحر 😉!", + "about_tagline": "\u0648\u062d\u062f\u0629 Xposed \u062a\u0647\u062f\u0641 \u0644\u062a\u062d\u0633\u064a\u0646 \u062a\u062c\u0631\u0628\u0629 Snapchat \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0643!", + "about_lead_developers_title": "\u0627\u0644\u0645\u0637\u0648\u0631\u0648\u0646 \u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0648\u0646", + "about_story_title": "\u0642\u0635\u062a\u0646\u0627", + "about_story": "\u062a\u0623\u0633\u0633\u062a PurrfectSnap \u0641\u064a 2 \u0623\u0643\u062a\u0648\u0628\u0631 2025\u060c \u0643\u062a\u0641\u0631\u0639 \u0645\u0646 SnapEnhance \u0628\u0648\u0627\u0633\u0637\u0629 \u039eT\u039eRNAL \u0645\u0639 \u0631\u0624\u064a\u0629 \u0644\u062a\u0642\u062f\u064a\u0645 \u062a\u062c\u0631\u0628\u0629 Snapchat \u0639\u0627\u0644\u064a\u0629 \u0627\u0644\u062c\u0648\u062f\u0629 \u0627\u0644\u062a\u064a \u064a\u0633\u062a\u062d\u0642\u0647\u0627 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645\u0648\u0646. \u0643\u0627\u0646 \u0645\u0646 \u0627\u0644\u0645\u0641\u062a\u0631\u0636 \u0623\u0646 \u064a\u0643\u0648\u0646 \u0647\u0630\u0627 \u0627\u0644\u062a\u0637\u0628\u064a\u0642 \u0645\u062c\u0631\u062f \u062a\u062d\u062f\u064a\u062b \u0628\u0633\u064a\u0637 \u0641\u064a \u0645\u0633\u062a\u0648\u062f\u0639 SnapEnhance\u060c \u0644\u0643\u0646\u0647 \u0633\u0631\u0639\u0627\u0646 \u0645\u0627 \u0623\u0635\u0628\u062d \u062a\u0637\u0628\u064a\u0642\u0627\u064b \u0645\u0646\u0641\u0635\u0644\u0627\u064b \u062d\u064a\u062b \u0627\u0633\u062a\u0645\u0631 \u0627\u0644\u0645\u0633\u0627\u0647\u0645\u0648\u0646 \u0641\u064a \u0625\u0636\u0627\u0641\u0629 \u0627\u0644\u0645\u064a\u0632\u0627\u062a. \u062b\u0645 \u0627\u0646\u0636\u0645 \u0627\u0644\u0645\u0637\u0648\u0631 \u0625\u0644\u0649 \u0627\u0644\u0641\u0631\u064a\u0642\u060c \u0648\u0633\u0631\u0639\u0627\u0646 \u0645\u0627 \u062d\u0642\u0642 \u0647\u0630\u0627 \u0627\u0644\u062a\u0637\u0628\u064a\u0642 \u0646\u062c\u0627\u062d\u0627\u064b \u0647\u0627\u0626\u0644\u0627\u064b. \u062a\u0644\u0642\u064a\u0646\u0627 \u0627\u0644\u0643\u062b\u064a\u0631 \u0645\u0646 \u0627\u0644\u062d\u0628 \u0648\u0627\u0644\u062f\u0639\u0645 \u0648\u062d\u0635\u0644\u0646\u0627 \u0639\u0644\u0649 \u0623\u0643\u062b\u0631 \u0645\u0646 1000 \u062a\u0646\u0632\u064a\u0644 \u0641\u064a \u064a\u0648\u0645\u064a\u0646 \u0641\u0642\u0637! \u0646\u0634\u0643\u0631 \u062c\u0645\u064a\u0639 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645\u064a\u0646 \u0648\u0627\u0644\u0645\u0633\u0627\u0647\u0645\u064a\u0646\u061b \u0628\u062f\u0648\u0646 \u062f\u0639\u0645\u0643\u0645\u060c \u0644\u0645 \u0646\u0643\u0646 \u0644\u0646\u0635\u0644 \u0625\u0644\u0649 \u0647\u0630\u0627 \u0627\u0644\u0645\u0643\u0627\u0646. \u0646\u0648\u062f \u0623\u064a\u0636\u0627\u064b \u0623\u0646 \u0646\u0639\u0631\u0628 \u0639\u0646 \u0634\u0643\u0631\u0646\u0627 \u0627\u0644\u062c\u0632\u064a\u0644 \u0644\u0640 rhunk\u060c \u0627\u0644\u0645\u0637\u0648\u0631 \u0627\u0644\u0631\u0626\u064a\u0633\u064a \u0644\u0640 SnapEnhance\u060c \u0641\u0628\u062f\u0648\u0646\u0647 \u0644\u0645 \u064a\u0643\u0646 \u0647\u0630\u0627 \u0627\u0644\u062a\u0637\u0628\u064a\u0642 \u0645\u0648\u062c\u0648\u062f\u0627\u064b \u062d\u062a\u0649. \u0646\u062d\u0646 \u0645\u0645\u062a\u0646\u0648\u0646 \u0644\u0647 \u0644\u0644\u063a\u0627\u064a\u0629. \u0623\u062e\u064a\u0631\u0627\u064b\u060c \u0646\u0648\u062f \u0623\u0646 \u0646\u0634\u0643\u0631 \u062c\u0645\u064a\u0639 \u0645\u0633\u0624\u0648\u0644\u064a\u0646\u0627\u060c \u0648\u0644\u0627 \u0633\u064a\u0645\u0627: CLASSIC GENIUS \u0648 Harry \u0648 Sujal \u0648 Zain \u0648 schrodingerspet\u060c \u0627\u0644\u0630\u064a\u0646 \u0643\u0627\u0646\u0648\u0627 \u0645\u0639\u0646\u0627 \u0645\u0646\u0630 \u0627\u0644\u0628\u062f\u0627\u064a\u0629. \u0646\u0648\u062f \u0623\u064a\u0636\u0627\u064b \u0623\u0646 \u0646\u0634\u0643\u0631 \u062c\u0645\u064a\u0639 \u0627\u0644\u0645\u062e\u062a\u0628\u0631\u064a\u0646\u060c \u0648\u0644\u0627 \u0633\u064a\u0645\u0627 Leo \u0648 Toxic\u060c \u0627\u0644\u0630\u064a\u0646 \u0627\u062e\u062a\u0628\u0631\u0648\u0627 \u0648\u0623\u0628\u0644\u063a\u0648\u0627 \u0639\u0646 \u0627\u0644\u0623\u062e\u0637\u0627\u0621 \u0628\u0627\u0633\u062a\u0645\u0631\u0627\u0631. \u0646\u062d\u0646 \u0645\u0645\u062a\u0646\u0648\u0646 \u0644\u0644\u063a\u0627\u064a\u0629 \u0644\u0645\u0633\u0627\u0647\u0645\u062a\u0643\u0645.", + "about_thanks_title": "\u0645\u0639 \u0627\u0644\u062d\u0628\u060c \u0641\u0631\u064a\u0642 PurrfectSnap", + "about_magic_toast": "\u0627\u0636\u063a\u0637 5 \u0645\u0631\u0627\u062a \u0641\u064a \u0647\u0630\u0647 \u0627\u0644\u0634\u0627\u0634\u0629 \u0644\u0631\u0624\u064a\u0629 \u0628\u0639\u0636 \u0627\u0644\u0633\u062d\u0631 \ud83d\ude09!", "github_button": "GitHub", - "telegram_button": "تيليجرام" + "telegram_button": "\u062a\u064a\u0644\u064a\u062c\u0631\u0627\u0645" }, "home_logs": { - "no_logs_hint": "لا تتوفر سجلات", - "refresh_hint": "اسحب للتحديث أو قم بتشغيل إجراء لرؤية مدخلات جديدة.", - "clear_logs_button": "مسح السجلات", - "export_logs_button": "تصدير السجلات", - "saving_logs_toast": "جاري حفظ السجلات، قد يستغرق هذا بعض الوقت...", - "saved_logs_success_toast": "تم حفظ السجلات بنجاح", - "saved_logs_failure_toast": "فشل حفظ السجلات", - "read_logs_failed_toast": "فشل قراءة السجلات!" + "no_logs_hint": "\u0644\u0627 \u062a\u062a\u0648\u0641\u0631 \u0633\u062c\u0644\u0627\u062a", + "refresh_hint": "\u0627\u0633\u062d\u0628 \u0644\u0644\u062a\u062d\u062f\u064a\u062b \u0623\u0648 \u0642\u0645 \u0628\u062a\u0634\u063a\u064a\u0644 \u0625\u062c\u0631\u0627\u0621 \u0644\u0631\u0624\u064a\u0629 \u0645\u062f\u062e\u0644\u0627\u062a \u062c\u062f\u064a\u062f\u0629.", + "clear_logs_button": "\u0645\u0633\u062d \u0627\u0644\u0633\u062c\u0644\u0627\u062a", + "export_logs_button": "\u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0633\u062c\u0644\u0627\u062a", + "saving_logs_toast": "\u062c\u0627\u0631\u064a \u062d\u0641\u0638 \u0627\u0644\u0633\u062c\u0644\u0627\u062a\u060c \u0642\u062f \u064a\u0633\u062a\u063a\u0631\u0642 \u0647\u0630\u0627 \u0628\u0639\u0636 \u0627\u0644\u0648\u0642\u062a...", + "saved_logs_success_toast": "\u062a\u0645 \u062d\u0641\u0638 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0628\u0646\u062c\u0627\u062d", + "saved_logs_failure_toast": "\u0641\u0634\u0644 \u062d\u0641\u0638 \u0627\u0644\u0633\u062c\u0644\u0627\u062a", + "read_logs_failed_toast": "\u0641\u0634\u0644 \u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0633\u062c\u0644\u0627\u062a!" }, "home_settings": { - "actions_title": "الإجراءات", - "message_logger_title": "مسجل الرسائل", - "debug_title": "تصحيح الأخطاء", - "success_toast": "تم!", - "message_logger_summary": "{messageCount} رسالة\n{storyCount} قصة", - "export_button": "تصدير", - "import_button": "استيراد", - "clear_button": "مسح", - "view_logger_history_button": "عرض سجل المسجل", - "message_logger_import_title": "استيراد مسجل الرسائل", - "message_logger_import_text": "سيؤدي الاستيراد إلى استبدال قاعدة بيانات مسجل الرسائل الحالية. هل تود المتابعة؟", - "ui_settings_title": "إعدادات واجهة المستخدم", - "haptic_feedback_label": "الاستجابة اللمسية", - "use_system_toasts_label": "استخدام رسائل النظام المنبثقة (Toasts)", - "updates_title": "التحديثات", - "auto_update_check": "فحص التحديث التلقائي", - "update_check_frequency_daily": "يومياً", - "update_check_frequency_weekly": "أسبوعياً", - "update_check_frequency_monthly": "شهرياً", - "update_channel_stable": "مستقر", - "update_channel_prerelease": "ما قبل الإصدار", - "update_notification_channel_name": "التحديثات", - "update_notification_channel_description": "احصل على إشعار عند توفر إصدارات جديدة", - "update_notification_title": "يتوفر تحديث جديد", - "update_notification_text": "اضغط لفتح PurrfectSnap وتنزيل أحدث بناء.", - "app_theme_title": "سمة التطبيق", - "theme_icon_description": "فتح منتقي السمات", - "theme_mode_system": "النظام", - "theme_mode_light": "فاتح", - "theme_mode_dark": "داكن", - "friend_notes_title": "ملاحظات الأصدقاء", - "friend_notes_description": "إدارة ونسخ ملاحظات الأصدقاء احتياطياً", - "friend_notes_no_notes_to_backup": "لا توجد ملاحظات لنسخها احتياطياً بعد", - "friend_notes_backup_success": "تم نسخ ملاحظات الأصدقاء احتياطياً", - "friend_notes_restore_success": "تمت استعادة ملاحظات الأصدقاء", - "backup_button": "نسخ احتياطي", - "restore_button": "استعادة", - "view_button": "عرض", - "customize_bottom_bar_title": "تخصيص الشريط السفلي", - "customize_bottom_bar_subtitle": "اختر علامات التبويب التي تظهر على شاشتك الرئيسية", - "available_tabs_title": "علامات التبويب المتاحة", - "reset_setup_title": "إعادة تعيين PurrfectSnap", - "reset_setup_action": "إعادة تعيين وإعادة تشغيل الإعداد", - "reset_setup_dialog_title": "هل أنت متأكد؟", - "reset_setup_dialog_text": "سيؤدي هذا إلى إعادة تعيين PurrfectSnap وإعادة تشغيل الإعداد.", - "reset_button": "إعادة تعيين", - "done_button": "تم", - "clear_friend_feed": "مسح موجز الأصدقاء", - "test_mode_label": "تمكين PurrAura", - "purr_aura_disable_title": "هل أنت متأكد؟", - "purr_aura_disable_text": "سيؤدي القيام بذلك إلى تعريض حسابك للخطر والتسبب في الحظر!", - "disable_feature_loading_label": "تعطيل تحميل الميزات", - "disable_auto_mapper_label": "تعطيل المعين التلقائي", - "disable_bypass_indicator_label": "تعطيل مؤشر التجاوز", - "open_file_failed_toast": "فشل فتح الملف! {message}", - "import_failed_toast": "فشل الاستيراد: {message}" + "actions_title": "\u0627\u0644\u0625\u062c\u0631\u0627\u0621\u0627\u062a", + "message_logger_title": "\u0645\u0633\u062c\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "debug_title": "\u062a\u0635\u062d\u064a\u062d \u0627\u0644\u0623\u062e\u0637\u0627\u0621", + "success_toast": "\u062a\u0645!", + "message_logger_summary": "{messageCount} \u0631\u0633\u0627\u0644\u0629\n{storyCount} \u0642\u0635\u0629", + "export_button": "\u062a\u0635\u062f\u064a\u0631", + "import_button": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f", + "clear_button": "\u0645\u0633\u062d", + "view_logger_history_button": "\u0639\u0631\u0636 \u0633\u062c\u0644 \u0627\u0644\u0645\u0633\u062c\u0644", + "message_logger_import_title": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0645\u0633\u062c\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "message_logger_import_text": "\u0633\u064a\u0624\u062f\u064a \u0627\u0644\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0625\u0644\u0649 \u0627\u0633\u062a\u0628\u062f\u0627\u0644 \u0642\u0627\u0639\u062f\u0629 \u0628\u064a\u0627\u0646\u0627\u062a \u0645\u0633\u062c\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062d\u0627\u0644\u064a\u0629. \u0647\u0644 \u062a\u0648\u062f \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629\u061f", + "ui_settings_title": "\u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0648\u0627\u062c\u0647\u0629 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "haptic_feedback_label": "\u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0627\u0644\u0644\u0645\u0633\u064a\u0629", + "use_system_toasts_label": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0646\u0638\u0627\u0645 \u0627\u0644\u0645\u0646\u0628\u062b\u0642\u0629 (Toasts)", + "updates_title": "\u0627\u0644\u062a\u062d\u062f\u064a\u062b\u0627\u062a", + "auto_update_check": "\u0641\u062d\u0635 \u0627\u0644\u062a\u062d\u062f\u064a\u062b \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "update_check_frequency_daily": "\u064a\u0648\u0645\u064a\u0627\u064b", + "update_check_frequency_weekly": "\u0623\u0633\u0628\u0648\u0639\u064a\u0627\u064b", + "update_check_frequency_monthly": "\u0634\u0647\u0631\u064a\u0627\u064b", + "update_channel_stable": "\u0645\u0633\u062a\u0642\u0631", + "update_channel_prerelease": "\u0645\u0627 \u0642\u0628\u0644 \u0627\u0644\u0625\u0635\u062f\u0627\u0631", + "update_notification_channel_name": "\u0627\u0644\u062a\u062d\u062f\u064a\u062b\u0627\u062a", + "update_notification_channel_description": "\u0627\u062d\u0635\u0644 \u0639\u0644\u0649 \u0625\u0634\u0639\u0627\u0631 \u0639\u0646\u062f \u062a\u0648\u0641\u0631 \u0625\u0635\u062f\u0627\u0631\u0627\u062a \u062c\u062f\u064a\u062f\u0629", + "update_notification_title": "\u064a\u062a\u0648\u0641\u0631 \u062a\u062d\u062f\u064a\u062b \u062c\u062f\u064a\u062f", + "update_notification_text": "\u0627\u0636\u063a\u0637 \u0644\u0641\u062a\u062d PurrfectSnap \u0648\u062a\u0646\u0632\u064a\u0644 \u0623\u062d\u062f\u062b \u0628\u0646\u0627\u0621.", + "app_theme_title": "\u0633\u0645\u0629 \u0627\u0644\u062a\u0637\u0628\u064a\u0642", + "theme_icon_description": "\u0641\u062a\u062d \u0645\u0646\u062a\u0642\u064a \u0627\u0644\u0633\u0645\u0627\u062a", + "theme_mode_system": "\u0627\u0644\u0646\u0638\u0627\u0645", + "theme_mode_light": "\u0641\u0627\u062a\u062d", + "theme_mode_dark": "\u062f\u0627\u0643\u0646", + "friend_notes_title": "\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "friend_notes_description": "\u0625\u062f\u0627\u0631\u0629 \u0648\u0646\u0633\u062e \u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0627\u062d\u062a\u064a\u0627\u0637\u064a\u0627\u064b", + "friend_notes_no_notes_to_backup": "\u0644\u0627 \u062a\u0648\u062c\u062f \u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0644\u0646\u0633\u062e\u0647\u0627 \u0627\u062d\u062a\u064a\u0627\u0637\u064a\u0627\u064b \u0628\u0639\u062f", + "friend_notes_backup_success": "\u062a\u0645 \u0646\u0633\u062e \u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0627\u062d\u062a\u064a\u0627\u0637\u064a\u0627\u064b", + "friend_notes_restore_success": "\u062a\u0645\u062a \u0627\u0633\u062a\u0639\u0627\u062f\u0629 \u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "backup_button": "\u0646\u0633\u062e \u0627\u062d\u062a\u064a\u0627\u0637\u064a", + "restore_button": "\u0627\u0633\u062a\u0639\u0627\u062f\u0629", + "view_button": "\u0639\u0631\u0636", + "customize_bottom_bar_title": "\u062a\u062e\u0635\u064a\u0635 \u0627\u0644\u0634\u0631\u064a\u0637 \u0627\u0644\u0633\u0641\u0644\u064a", + "customize_bottom_bar_subtitle": "\u0627\u062e\u062a\u0631 \u0639\u0644\u0627\u0645\u0627\u062a \u0627\u0644\u062a\u0628\u0648\u064a\u0628 \u0627\u0644\u062a\u064a \u062a\u0638\u0647\u0631 \u0639\u0644\u0649 \u0634\u0627\u0634\u062a\u0643 \u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629", + "available_tabs_title": "\u0639\u0644\u0627\u0645\u0627\u062a \u0627\u0644\u062a\u0628\u0648\u064a\u0628 \u0627\u0644\u0645\u062a\u0627\u062d\u0629", + "reset_setup_title": "\u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646 PurrfectSnap", + "reset_setup_action": "\u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646 \u0648\u0625\u0639\u0627\u062f\u0629 \u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0625\u0639\u062f\u0627\u062f", + "reset_setup_dialog_title": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f\u061f", + "reset_setup_dialog_text": "\u0633\u064a\u0624\u062f\u064a \u0647\u0630\u0627 \u0625\u0644\u0649 \u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646 PurrfectSnap \u0648\u0625\u0639\u0627\u062f\u0629 \u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0625\u0639\u062f\u0627\u062f.", + "reset_button": "\u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646", + "done_button": "\u062a\u0645", + "clear_friend_feed": "\u0645\u0633\u062d \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "test_mode_label": "\u062a\u0645\u0643\u064a\u0646 PurrAura", + "purr_aura_disable_title": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f\u061f", + "purr_aura_disable_text": "\u0633\u064a\u0624\u062f\u064a \u0627\u0644\u0642\u064a\u0627\u0645 \u0628\u0630\u0644\u0643 \u0625\u0644\u0649 \u062a\u0639\u0631\u064a\u0636 \u062d\u0633\u0627\u0628\u0643 \u0644\u0644\u062e\u0637\u0631 \u0648\u0627\u0644\u062a\u0633\u0628\u0628 \u0641\u064a \u0627\u0644\u062d\u0638\u0631!", + "disable_feature_loading_label": "\u062a\u0639\u0637\u064a\u0644 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u064a\u0632\u0627\u062a", + "disable_auto_mapper_label": "\u062a\u0639\u0637\u064a\u0644 \u0627\u0644\u0645\u0639\u064a\u0646 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "disable_bypass_indicator_label": "\u062a\u0639\u0637\u064a\u0644 \u0645\u0624\u0634\u0631 \u0627\u0644\u062a\u062c\u0627\u0648\u0632", + "open_file_failed_toast": "\u0641\u0634\u0644 \u0641\u062a\u062d \u0627\u0644\u0645\u0644\u0641! {message}", + "import_failed_toast": "\u0641\u0634\u0644 \u0627\u0644\u0627\u0633\u062a\u064a\u0631\u0627\u062f: {message}" }, "retro_flight": { "title": "Retro Flight", - "game_over_label": "انتهت اللعبة", - "restart_button": "إعادة تشغيل", - "left_button": "يسار", - "right_button": "يمين" + "game_over_label": "\u0627\u0646\u062a\u0647\u062a \u0627\u0644\u0644\u0639\u0628\u0629", + "restart_button": "\u0625\u0639\u0627\u062f\u0629 \u062a\u0634\u063a\u064a\u0644", + "left_button": "\u064a\u0633\u0627\u0631", + "right_button": "\u064a\u0645\u064a\u0646" }, "tasks": { - "no_tasks": "لا توجد مهام", - "merge_button": "دمج", - "summary_active": "{active} نشط \u00b7 {recent} حديث", - "summary_idle": "خامل \u00b7 {recent} حديث", - "running_count": "{count} قيد التشغيل", - "clear_button_description": "مسح المهام", - "failed_to_open_file": "فشل فتح الملف", - "merge_files_toast": "جاري دمج {count} ملفات", - "remove_selected_tasks_title": "هل أنت متأكد أنك تريد إزالة المهام المحددة؟", - "remove_all_tasks_title": "هل أنت متأكد أنك تريد إزالة جميع المهام؟", - "delete_files_option": "حذف الملفات أيضاً", - "delete_files_option_hint": "إزالة الملفات المحملة من الجهاز أيضاً", - "remove_selected_tasks_confirm": "إزالة {count} مهام؟", - "remove_all_tasks_confirm": "إزالة جميع المهام؟" + "no_tasks": "\u0644\u0627 \u062a\u0648\u062c\u062f \u0645\u0647\u0627\u0645", + "merge_button": "\u062f\u0645\u062c", + "summary_active": "{active} \u0646\u0634\u0637 \u00b7 {recent} \u062d\u062f\u064a\u062b", + "summary_idle": "\u062e\u0627\u0645\u0644 \u00b7 {recent} \u062d\u062f\u064a\u062b", + "running_count": "{count} \u0642\u064a\u062f \u0627\u0644\u062a\u0634\u063a\u064a\u0644", + "clear_button_description": "\u0645\u0633\u062d \u0627\u0644\u0645\u0647\u0627\u0645", + "failed_to_open_file": "\u0641\u0634\u0644 \u0641\u062a\u062d \u0627\u0644\u0645\u0644\u0641", + "merge_files_toast": "\u062c\u0627\u0631\u064a \u062f\u0645\u062c {count} \u0645\u0644\u0641\u0627\u062a", + "remove_selected_tasks_title": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0645\u0647\u0627\u0645 \u0627\u0644\u0645\u062d\u062f\u062f\u0629\u061f", + "remove_all_tasks_title": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u0625\u0632\u0627\u0644\u0629 \u062c\u0645\u064a\u0639 \u0627\u0644\u0645\u0647\u0627\u0645\u061f", + "delete_files_option": "\u062d\u0630\u0641 \u0627\u0644\u0645\u0644\u0641\u0627\u062a \u0623\u064a\u0636\u0627\u064b", + "delete_files_option_hint": "\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0645\u0644\u0641\u0627\u062a \u0627\u0644\u0645\u062d\u0645\u0644\u0629 \u0645\u0646 \u0627\u0644\u062c\u0647\u0627\u0632 \u0623\u064a\u0636\u0627\u064b", + "remove_selected_tasks_confirm": "\u0625\u0632\u0627\u0644\u0629 {count} \u0645\u0647\u0627\u0645\u061f", + "remove_all_tasks_confirm": "\u0625\u0632\u0627\u0644\u0629 \u062c\u0645\u064a\u0639 \u0627\u0644\u0645\u0647\u0627\u0645\u061f" }, "features": { - "disabled": "معطل", - "export_option": "تصدير", - "import_option": "استيراد", - "reset_option": "إعادة تعيين", - "config_export_success_toast": "تم تصدير التكوين بنجاح", - "config_import_success_toast": "تم استيراد التكوين بنجاح", - "config_import_failure_toast": "فشل استيراد التكوين {error}", - "config_export_failure_toast": "فشل تصدير التكوين {error}", - "saved_config_snackbar": "تم حفظ التكوين", - "older_required": "تتطلب هذه الميزة Snapchat v{version} أو أقدم لتعمل بشكل صحيح", - "newer_required": "تتطلب هذه الميزة Snapchat v{version} أو أحدث لتعمل بشكل صحيح", - "search_button": "بحث", - "search_results_count": "{count} رسالة", - "clear_history": "مسح سجل البحث", - "subtitle": "البحث وإدارة الميزات" + "disabled": "\u0645\u0639\u0637\u0644", + "export_option": "\u062a\u0635\u062f\u064a\u0631", + "import_option": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f", + "reset_option": "\u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646", + "config_export_success_toast": "\u062a\u0645 \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u062a\u0643\u0648\u064a\u0646 \u0628\u0646\u062c\u0627\u062d", + "config_import_success_toast": "\u062a\u0645 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u062a\u0643\u0648\u064a\u0646 \u0628\u0646\u062c\u0627\u062d", + "config_import_failure_toast": "\u0641\u0634\u0644 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u062a\u0643\u0648\u064a\u0646 {error}", + "config_export_failure_toast": "\u0641\u0634\u0644 \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u062a\u0643\u0648\u064a\u0646 {error}", + "saved_config_snackbar": "\u062a\u0645 \u062d\u0641\u0638 \u0627\u0644\u062a\u0643\u0648\u064a\u0646", + "older_required": "\u062a\u062a\u0637\u0644\u0628 \u0647\u0630\u0647 \u0627\u0644\u0645\u064a\u0632\u0629 Snapchat v{version} \u0623\u0648 \u0623\u0642\u062f\u0645 \u0644\u062a\u0639\u0645\u0644 \u0628\u0634\u0643\u0644 \u0635\u062d\u064a\u062d", + "newer_required": "\u062a\u062a\u0637\u0644\u0628 \u0647\u0630\u0647 \u0627\u0644\u0645\u064a\u0632\u0629 Snapchat v{version} \u0623\u0648 \u0623\u062d\u062f\u062b \u0644\u062a\u0639\u0645\u0644 \u0628\u0634\u0643\u0644 \u0635\u062d\u064a\u062d", + "search_button": "\u0628\u062d\u062b", + "search_results_count": "{count} \u0631\u0633\u0627\u0644\u0629", + "clear_history": "\u0645\u0633\u062d \u0633\u062c\u0644 \u0627\u0644\u0628\u062d\u062b", + "subtitle": "\u0627\u0644\u0628\u062d\u062b \u0648\u0625\u062f\u0627\u0631\u0629 \u0627\u0644\u0645\u064a\u0632\u0627\u062a", + "digits_only_toast": "\u064a\u064f\u0633\u0645\u062d \u0628\u0627\u0644\u0623\u0631\u0642\u0627\u0645 \u0641\u0642\u0637." }, "bypass_status": { - "active": "PurrAura نشط", - "inactive": "PurrAura غير نشط" + "active": "PurrAura \u0646\u0634\u0637", + "inactive": "PurrAura \u063a\u064a\u0631 \u0646\u0634\u0637" }, "manage_rule_feature": { - "disable_state_option": "معطل", - "disable_state_subtext": "لن يتأثر أي أصدقاء/مجموعات", - "whitelist_state_option": "لا أحد باستثناء ...", - "whitelist_state_subtext": "سيتأثر {count} صديق/مجموعة فقط بهذه القاعدة", - "whitelist_state_button": "تحديد الأصدقاء/المجموعات المسموح بها", - "blacklist_state_option": "الجميع باستثناء ...", - "blacklist_state_subtext": "سيتأثر الجميع باستثناء {count} صديق/مجموعة بهذه القاعدة", - "blacklist_state_button": "تحديد الأصدقاء/المجموعات المستبعدة", - "clear_list_button": "مسح قائمة الأصدقاء/المجموعات", - "dialog_clear_confirmation_text": "هل أنت متأكد أنك تريد مسح القائمة؟", - "dialog_clear_confirm_button": "مسح", - "dialog_clear_cancel_button": "إلغاء" + "disable_state_option": "\u0645\u0639\u0637\u0644", + "disable_state_subtext": "\u0644\u0646 \u064a\u062a\u0623\u062b\u0631 \u0623\u064a \u0623\u0635\u062f\u0642\u0627\u0621/\u0645\u062c\u0645\u0648\u0639\u0627\u062a", + "whitelist_state_option": "\u0644\u0627 \u0623\u062d\u062f \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 ...", + "whitelist_state_subtext": "\u0633\u064a\u062a\u0623\u062b\u0631 {count} \u0635\u062f\u064a\u0642/\u0645\u062c\u0645\u0648\u0639\u0629 \u0641\u0642\u0637 \u0628\u0647\u0630\u0647 \u0627\u0644\u0642\u0627\u0639\u062f\u0629", + "whitelist_state_button": "\u062a\u062d\u062f\u064a\u062f \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621/\u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0627\u062a \u0627\u0644\u0645\u0633\u0645\u0648\u062d \u0628\u0647\u0627", + "blacklist_state_option": "\u0627\u0644\u062c\u0645\u064a\u0639 \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 ...", + "blacklist_state_subtext": "\u0633\u064a\u062a\u0623\u062b\u0631 \u0627\u0644\u062c\u0645\u064a\u0639 \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 {count} \u0635\u062f\u064a\u0642/\u0645\u062c\u0645\u0648\u0639\u0629 \u0628\u0647\u0630\u0647 \u0627\u0644\u0642\u0627\u0639\u062f\u0629", + "blacklist_state_button": "\u062a\u062d\u062f\u064a\u062f \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621/\u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0627\u062a \u0627\u0644\u0645\u0633\u062a\u0628\u0639\u062f\u0629", + "clear_list_button": "\u0645\u0633\u062d \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621/\u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0627\u062a", + "dialog_clear_confirmation_text": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u0645\u0633\u062d \u0627\u0644\u0642\u0627\u0626\u0645\u0629\u061f", + "dialog_clear_confirm_button": "\u0645\u0633\u062d", + "dialog_clear_cancel_button": "\u0625\u0644\u063a\u0627\u0621" }, "social": { - "friends_tab": "الأصدقاء", - "groups_tab": "المجموعات", - "search_button_description": "بحث", - "close_search_button_description": "إغلاق البحث", - "clear_search_button_description": "مسح البحث", - "empty_hint": "قائمتك فارغة الآن", - "friends_empty_title": "لم تتم إضافة أصدقاء بعد", - "groups_empty_title": "لم تتم مزامنة المجموعات بعد", - "streaks_expiration_short": "{hours}س", - "social_tagline": "إدارة النطاقات والستريك والمعاينات", - "social_empty_hint": "اضغط على زر + لمزامنة الأصدقاء أو المجموعات." + "friends_tab": "\u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "groups_tab": "\u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0627\u062a", + "search_button_description": "\u0628\u062d\u062b", + "close_search_button_description": "\u0625\u063a\u0644\u0627\u0642 \u0627\u0644\u0628\u062d\u062b", + "clear_search_button_description": "\u0645\u0633\u062d \u0627\u0644\u0628\u062d\u062b", + "empty_hint": "\u0642\u0627\u0626\u0645\u062a\u0643 \u0641\u0627\u0631\u063a\u0629 \u0627\u0644\u0622\u0646", + "friends_empty_title": "\u0644\u0645 \u062a\u062a\u0645 \u0625\u0636\u0627\u0641\u0629 \u0623\u0635\u062f\u0642\u0627\u0621 \u0628\u0639\u062f", + "groups_empty_title": "\u0644\u0645 \u062a\u062a\u0645 \u0645\u0632\u0627\u0645\u0646\u0629 \u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0627\u062a \u0628\u0639\u062f", + "streaks_expiration_short": "{hours}\u0633", + "social_tagline": "\u0625\u062f\u0627\u0631\u0629 \u0627\u0644\u0646\u0637\u0627\u0642\u0627\u062a \u0648\u0627\u0644\u0633\u062a\u0631\u064a\u0643 \u0648\u0627\u0644\u0645\u0639\u0627\u064a\u0646\u0627\u062a", + "social_empty_hint": "\u0627\u0636\u063a\u0637 \u0639\u0644\u0649 \u0632\u0631 + \u0644\u0645\u0632\u0627\u0645\u0646\u0629 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0623\u0648 \u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0627\u062a." }, "manage_scope": { - "manage_scope_title": "إدارة", - "logged_stories_button": "عرض القصص المسجلة", - "e2ee_title": "التشفير من طرف لطرف", - "e2ee_subtitle": "إدارة مفتاحك المشترك لهذا الصديق.", - "export_base64_button": "تصدير Base64", - "import_base64_button": "استيراد Base64", - "invalid_key_size_32_bytes": "حجم مفتاح غير صالح. قدم مفتاحاً بحجم 32 بايت.", - "successfully_imported_key": "تم استيراد المفتاح بنجاح.", - "failed_to_import_key": "فشل استيراد المفتاح: {message}", - "rules_title": "القواعد", - "participants_text": "{count} مشارك", - "not_found": "غير موجود", - "streaks_title": "الستريك (Streaks)", - "streaks_length_text": "الطول: {length}", - "streaks_expiration_text": "ينتهي في {eta}", - "streaks_expiration_text_expired": "منتهي الصلاحية", - "reminder_button": "تعيين تذكير", - "delete_scope_confirm_dialog_title": "هل أنت متأكد أنك تريد حذف {scope}؟", - "notes_placeholder": "اضغط لإضافة ملاحظة" + "manage_scope_title": "\u0625\u062f\u0627\u0631\u0629", + "logged_stories_button": "\u0639\u0631\u0636 \u0627\u0644\u0642\u0635\u0635 \u0627\u0644\u0645\u0633\u062c\u0644\u0629", + "e2ee_title": "\u0627\u0644\u062a\u0634\u0641\u064a\u0631 \u0645\u0646 \u0637\u0631\u0641 \u0644\u0637\u0631\u0641", + "e2ee_subtitle": "\u0625\u062f\u0627\u0631\u0629 \u0645\u0641\u062a\u0627\u062d\u0643 \u0627\u0644\u0645\u0634\u062a\u0631\u0643 \u0644\u0647\u0630\u0627 \u0627\u0644\u0635\u062f\u064a\u0642.", + "export_base64_button": "\u062a\u0635\u062f\u064a\u0631 Base64", + "import_base64_button": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f Base64", + "invalid_key_size_32_bytes": "\u062d\u062c\u0645 \u0645\u0641\u062a\u0627\u062d \u063a\u064a\u0631 \u0635\u0627\u0644\u062d. \u0642\u062f\u0645 \u0645\u0641\u062a\u0627\u062d\u0627\u064b \u0628\u062d\u062c\u0645 32 \u0628\u0627\u064a\u062a.", + "successfully_imported_key": "\u062a\u0645 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0645\u0641\u062a\u0627\u062d \u0628\u0646\u062c\u0627\u062d.", + "failed_to_import_key": "\u0641\u0634\u0644 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0645\u0641\u062a\u0627\u062d: {message}", + "rules_title": "\u0627\u0644\u0642\u0648\u0627\u0639\u062f", + "participants_text": "{count} \u0645\u0634\u0627\u0631\u0643", + "not_found": "\u063a\u064a\u0631 \u0645\u0648\u062c\u0648\u062f", + "streaks_title": "\u0627\u0644\u0633\u062a\u0631\u064a\u0643 (Streaks)", + "streaks_length_text": "\u0627\u0644\u0637\u0648\u0644: {length}", + "streaks_expiration_text": "\u064a\u0646\u062a\u0647\u064a \u0641\u064a {eta}", + "streaks_expiration_text_expired": "\u0645\u0646\u062a\u0647\u064a \u0627\u0644\u0635\u0644\u0627\u062d\u064a\u0629", + "reminder_button": "\u062a\u0639\u064a\u064a\u0646 \u062a\u0630\u0643\u064a\u0631", + "delete_scope_confirm_dialog_title": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u062d\u0630\u0641 {scope}\u061f", + "notes_placeholder": "\u0627\u0636\u063a\u0637 \u0644\u0625\u0636\u0627\u0641\u0629 \u0645\u0644\u0627\u062d\u0638\u0629" }, "logged_stories": { - "story_failed_to_load": "فشل التحميل", - "no_stories": "لم يتم العثور على قصص", - "save_from_cache_button": "حفظ من الذاكرة المؤقتة" + "story_failed_to_load": "\u0641\u0634\u0644 \u0627\u0644\u062a\u062d\u0645\u064a\u0644", + "no_stories": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0642\u0635\u0635", + "save_from_cache_button": "\u062d\u0641\u0638 \u0645\u0646 \u0627\u0644\u0630\u0627\u0643\u0631\u0629 \u0627\u0644\u0645\u0624\u0642\u062a\u0629" }, "messaging_preview": { - "bridge_connection_failed": "فشل الاتصال بالجسر. تأكد من أن Snapchat يعمل في الخلفية", - "bridge_connection_error": "فشل الاتصال بالجسر. تأكد من أن Snapchat يعمل في الخلفية", - "bridge_init_failed": "فشل تهيئة جسر المراسلة. تأكد من أن Snapchat يعمل في الخلفية", - "message_fetch_failed": "فشل جلب الرسائل", - "no_message_hint": "لا توجد رسالة", - "sender_unknown": "غير معروف", - "sender_you": "أنت", - "sender_friend": "صديق", - "subtitle": "استمر بالضغط للاختيار", - "actions_title": "إجراءات المحادثة", - "choose_message_types_subtitle": "اختر أنواع الرسائل", - "save_selection_option": "حفظ المحدد", - "save_all_option": "حفظ الكل", - "save_selected_messages_subtitle": "حفظ الرسائل المحددة", - "save_by_content_type_subtitle": "حفظ حسب نوع المحتوى", - "unsave_selection_option": "إلغاء حفظ المحدد", - "unsave_all_option": "إلغاء حفظ الكل", - "unsave_selected_messages_subtitle": "إلغاء حفظ الرسائل المحددة", - "unsave_by_content_type_subtitle": "إلغاء حفظ حسب نوع المحتوى", - "mark_selection_as_seen_option": "وضع علامة \"تمت المشاهدة\" على المحدد", - "mark_all_as_seen_option": "وضع علامة \"تمت المشاهدة\" على كل الـ Snaps", - "mark_as_seen_subtitle": "يضع علامة تمت المشاهدة على الـ snaps", - "delete_selection_option": "حذف المحدد", - "delete_all_option": "حذف الكل", - "delete_selected_messages_subtitle": "حذف الرسائل المحددة", - "delete_by_content_type_subtitle": "حذف حسب نوع المحتوى", - "processed_message_toast": "تمت معالجة {count} رسالة", - "processed_messages_toast": "تمت معالجة {count} رسالة", - "processed_messages_text": "تمت معالجة {count}", - "close_button_description": "مسح التحديد" + "bridge_connection_failed": "\u0641\u0634\u0644 \u0627\u0644\u0627\u062a\u0635\u0627\u0644 \u0628\u0627\u0644\u062c\u0633\u0631. \u062a\u0623\u0643\u062f \u0645\u0646 \u0623\u0646 Snapchat \u064a\u0639\u0645\u0644 \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629", + "bridge_connection_error": "\u0641\u0634\u0644 \u0627\u0644\u0627\u062a\u0635\u0627\u0644 \u0628\u0627\u0644\u062c\u0633\u0631. \u062a\u0623\u0643\u062f \u0645\u0646 \u0623\u0646 Snapchat \u064a\u0639\u0645\u0644 \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629", + "bridge_init_failed": "\u0641\u0634\u0644 \u062a\u0647\u064a\u0626\u0629 \u062c\u0633\u0631 \u0627\u0644\u0645\u0631\u0627\u0633\u0644\u0629. \u062a\u0623\u0643\u062f \u0645\u0646 \u0623\u0646 Snapchat \u064a\u0639\u0645\u0644 \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629", + "message_fetch_failed": "\u0641\u0634\u0644 \u062c\u0644\u0628 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "no_message_hint": "\u0644\u0627 \u062a\u0648\u062c\u062f \u0631\u0633\u0627\u0644\u0629", + "sender_unknown": "\u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641", + "sender_you": "\u0623\u0646\u062a", + "sender_friend": "\u0635\u062f\u064a\u0642", + "subtitle": "\u0627\u0633\u062a\u0645\u0631 \u0628\u0627\u0644\u0636\u063a\u0637 \u0644\u0644\u0627\u062e\u062a\u064a\u0627\u0631", + "actions_title": "\u0625\u062c\u0631\u0627\u0621\u0627\u062a \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "choose_message_types_subtitle": "\u0627\u062e\u062a\u0631 \u0623\u0646\u0648\u0627\u0639 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "save_selection_option": "\u062d\u0641\u0638 \u0627\u0644\u0645\u062d\u062f\u062f", + "save_all_option": "\u062d\u0641\u0638 \u0627\u0644\u0643\u0644", + "save_selected_messages_subtitle": "\u062d\u0641\u0638 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u062d\u062f\u062f\u0629", + "save_by_content_type_subtitle": "\u062d\u0641\u0638 \u062d\u0633\u0628 \u0646\u0648\u0639 \u0627\u0644\u0645\u062d\u062a\u0648\u0649", + "unsave_selection_option": "\u0625\u0644\u063a\u0627\u0621 \u062d\u0641\u0638 \u0627\u0644\u0645\u062d\u062f\u062f", + "unsave_all_option": "\u0625\u0644\u063a\u0627\u0621 \u062d\u0641\u0638 \u0627\u0644\u0643\u0644", + "unsave_selected_messages_subtitle": "\u0625\u0644\u063a\u0627\u0621 \u062d\u0641\u0638 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u062d\u062f\u062f\u0629", + "unsave_by_content_type_subtitle": "\u0625\u0644\u063a\u0627\u0621 \u062d\u0641\u0638 \u062d\u0633\u0628 \u0646\u0648\u0639 \u0627\u0644\u0645\u062d\u062a\u0648\u0649", + "mark_selection_as_seen_option": "\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\" \u0639\u0644\u0649 \u0627\u0644\u0645\u062d\u062f\u062f", + "mark_all_as_seen_option": "\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\" \u0639\u0644\u0649 \u0643\u0644 \u0627\u0644\u0640 Snaps", + "mark_as_seen_subtitle": "\u064a\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629 \u0639\u0644\u0649 \u0627\u0644\u0640 snaps", + "delete_selection_option": "\u062d\u0630\u0641 \u0627\u0644\u0645\u062d\u062f\u062f", + "delete_all_option": "\u062d\u0630\u0641 \u0627\u0644\u0643\u0644", + "delete_selected_messages_subtitle": "\u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u062d\u062f\u062f\u0629", + "delete_by_content_type_subtitle": "\u062d\u0630\u0641 \u062d\u0633\u0628 \u0646\u0648\u0639 \u0627\u0644\u0645\u062d\u062a\u0648\u0649", + "processed_message_toast": "\u062a\u0645\u062a \u0645\u0639\u0627\u0644\u062c\u0629 {count} \u0631\u0633\u0627\u0644\u0629", + "processed_messages_toast": "\u062a\u0645\u062a \u0645\u0639\u0627\u0644\u062c\u0629 {count} \u0631\u0633\u0627\u0644\u0629", + "processed_messages_text": "\u062a\u0645\u062a \u0645\u0639\u0627\u0644\u062c\u0629 {count}", + "close_button_description": "\u0645\u0633\u062d \u0627\u0644\u062a\u062d\u062f\u064a\u062f" }, "logger_history": { - "list_friend_format": "صديق {name}", - "list_group_format": "مجموعة {name}", - "no_more_messages": "لا مزيد من الرسائل", - "reverse_order_checkbox": "عكس الترتيب", - "chat_attachment": "مرفق {index}", - "empty_message": "رسالة دردشة فارغة", - "message_parse_failed": "فشل تحليل الرسالة", - "unknown_sender": "مرسل غير معروف", - "download_attachment_failed_toast": "فشل تنزيل المرفق" + "list_friend_format": "\u0635\u062f\u064a\u0642 {name}", + "list_group_format": "\u0645\u062c\u0645\u0648\u0639\u0629 {name}", + "no_more_messages": "\u0644\u0627 \u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "reverse_order_checkbox": "\u0639\u0643\u0633 \u0627\u0644\u062a\u0631\u062a\u064a\u0628", + "chat_attachment": "\u0645\u0631\u0641\u0642 {index}", + "empty_message": "\u0631\u0633\u0627\u0644\u0629 \u062f\u0631\u062f\u0634\u0629 \u0641\u0627\u0631\u063a\u0629", + "message_parse_failed": "\u0641\u0634\u0644 \u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "unknown_sender": "\u0645\u0631\u0633\u0644 \u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641", + "download_attachment_failed_toast": "\u0641\u0634\u0644 \u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0645\u0631\u0641\u0642" }, "file_imports": { - "import_file_button": "استيراد ملف", - "file_not_found": "لم يتم العثور على الملف", - "file_import_failed": "فشل استيراد الملف: {error}", - "file_imported": "تم استيراد الملف بنجاح", - "file_delete_failed": "فشل حذف الملف", - "no_files_hint": "هنا يمكنك استيراد الملفات للاستخدام في Snapchat. اضغط على الزر أدناه لاستيراد ملف." + "import_file_button": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0645\u0644\u0641", + "file_not_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0627\u0644\u0645\u0644\u0641", + "file_import_failed": "\u0641\u0634\u0644 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0645\u0644\u0641: {error}", + "file_imported": "\u062a\u0645 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0645\u0644\u0641 \u0628\u0646\u062c\u0627\u062d", + "file_delete_failed": "\u0641\u0634\u0644 \u062d\u0630\u0641 \u0627\u0644\u0645\u0644\u0641", + "no_files_hint": "\u0647\u0646\u0627 \u064a\u0645\u0643\u0646\u0643 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0645\u0644\u0641\u0627\u062a \u0644\u0644\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0641\u064a Snapchat. \u0627\u0636\u063a\u0637 \u0639\u0644\u0649 \u0627\u0644\u0632\u0631 \u0623\u062f\u0646\u0627\u0647 \u0644\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0645\u0644\u0641." }, "better_location": { - "spoofed_coordinates_title": "خط العرض {latitude}، خط الطول {longitude}", - "save_coordinates_dialog_title": "حفظ الإحداثيات", - "saved_name_dialog_hint": "الاسم المحفوظ", - "latitude_dialog_hint": "خط العرض", - "longitude_dialog_hint": "خط الطول", - "save_dialog_button": "حفظ", - "choose_location_button": "اختر موقعاً", - "search_or_tap_map_hint": "ابحث أو اضغط على الخريطة", - "search_location_placeholder": "بحث عن موقع...", - "search_icon_description": "بحث", - "searching_label": "جاري البحث...", - "manual_coordinates_hint": "ضبط الإحداثيات يدوياً.", - "saved_coordinates_subtitle": "إدارة مواقع التزييف المحفوظة", - "teleport_to_friend_button": "انتقال فوري إلى صديق", - "spoof_location_toggle": "تزييف الموقع", - "suspend_location_updates": "تعليق تحديثات الموقع", - "saved_coordinates_title": "الإحداثيات المحفوظة", - "no_saved_coordinates_hint": "لا توجد إحداثيات محفوظة", - "delete_dialog_title": "حذف الإحداثية المحفوظة", - "delete_dialog_message": "هل أنت متأكد أنك تريد حذف هذه الإحداثية المحفوظة؟", - "teleport_to_friend_title": "انتقال فوري إلى صديق", - "search_bar": "بحث", - "no_friends_map": "لا يوجد أصدقاء على الخريطة", - "no_friends_found": "لم يتم العثور على أصدقاء", - "include_saved_locations": "تضمين المواقع المحفوظة", - "include_saved_locations_description": "تصدير إحداثيات المواقع المحفوظة الخاصة بك", - "location_search_provider_title": "موفر البحث عن الموقع", - "google_maps_api_key_title": "مفتاح Google Maps API", - "option_osm": "OpenStreetMap (مجاني)", - "option_google_maps": "خرائط Google" + "spoofed_coordinates_title": "\u062e\u0637 \u0627\u0644\u0639\u0631\u0636 {latitude}\u060c \u062e\u0637 \u0627\u0644\u0637\u0648\u0644 {longitude}", + "save_coordinates_dialog_title": "\u062d\u0641\u0638 \u0627\u0644\u0625\u062d\u062f\u0627\u062b\u064a\u0627\u062a", + "saved_name_dialog_hint": "\u0627\u0644\u0627\u0633\u0645 \u0627\u0644\u0645\u062d\u0641\u0648\u0638", + "latitude_dialog_hint": "\u062e\u0637 \u0627\u0644\u0639\u0631\u0636", + "longitude_dialog_hint": "\u062e\u0637 \u0627\u0644\u0637\u0648\u0644", + "save_dialog_button": "\u062d\u0641\u0638", + "choose_location_button": "\u0627\u062e\u062a\u0631 \u0645\u0648\u0642\u0639\u0627\u064b", + "search_or_tap_map_hint": "\u0627\u0628\u062d\u062b \u0623\u0648 \u0627\u0636\u063a\u0637 \u0639\u0644\u0649 \u0627\u0644\u062e\u0631\u064a\u0637\u0629", + "search_location_placeholder": "\u0628\u062d\u062b \u0639\u0646 \u0645\u0648\u0642\u0639...", + "search_icon_description": "\u0628\u062d\u062b", + "searching_label": "\u062c\u0627\u0631\u064a \u0627\u0644\u0628\u062d\u062b...", + "manual_coordinates_hint": "\u0636\u0628\u0637 \u0627\u0644\u0625\u062d\u062f\u0627\u062b\u064a\u0627\u062a \u064a\u062f\u0648\u064a\u0627\u064b.", + "saved_coordinates_subtitle": "\u0625\u062f\u0627\u0631\u0629 \u0645\u0648\u0627\u0642\u0639 \u0627\u0644\u062a\u0632\u064a\u064a\u0641 \u0627\u0644\u0645\u062d\u0641\u0648\u0638\u0629", + "teleport_to_friend_button": "\u0627\u0646\u062a\u0642\u0627\u0644 \u0641\u0648\u0631\u064a \u0625\u0644\u0649 \u0635\u062f\u064a\u0642", + "spoof_location_toggle": "\u062a\u0632\u064a\u064a\u0641 \u0627\u0644\u0645\u0648\u0642\u0639", + "suspend_location_updates": "\u062a\u0639\u0644\u064a\u0642 \u062a\u062d\u062f\u064a\u062b\u0627\u062a \u0627\u0644\u0645\u0648\u0642\u0639", + "saved_coordinates_title": "\u0627\u0644\u0625\u062d\u062f\u0627\u062b\u064a\u0627\u062a \u0627\u0644\u0645\u062d\u0641\u0648\u0638\u0629", + "no_saved_coordinates_hint": "\u0644\u0627 \u062a\u0648\u062c\u062f \u0625\u062d\u062f\u0627\u062b\u064a\u0627\u062a \u0645\u062d\u0641\u0648\u0638\u0629", + "delete_dialog_title": "\u062d\u0630\u0641 \u0627\u0644\u0625\u062d\u062f\u0627\u062b\u064a\u0629 \u0627\u0644\u0645\u062d\u0641\u0648\u0638\u0629", + "delete_dialog_message": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u062d\u0630\u0641 \u0647\u0630\u0647 \u0627\u0644\u0625\u062d\u062f\u0627\u062b\u064a\u0629 \u0627\u0644\u0645\u062d\u0641\u0648\u0638\u0629\u061f", + "teleport_to_friend_title": "\u0627\u0646\u062a\u0642\u0627\u0644 \u0641\u0648\u0631\u064a \u0625\u0644\u0649 \u0635\u062f\u064a\u0642", + "search_bar": "\u0628\u062d\u062b", + "no_friends_map": "\u0644\u0627 \u064a\u0648\u062c\u062f \u0623\u0635\u062f\u0642\u0627\u0621 \u0639\u0644\u0649 \u0627\u0644\u062e\u0631\u064a\u0637\u0629", + "no_friends_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0623\u0635\u062f\u0642\u0627\u0621", + "include_saved_locations": "\u062a\u0636\u0645\u064a\u0646 \u0627\u0644\u0645\u0648\u0627\u0642\u0639 \u0627\u0644\u0645\u062d\u0641\u0648\u0638\u0629", + "include_saved_locations_description": "\u062a\u0635\u062f\u064a\u0631 \u0625\u062d\u062f\u0627\u062b\u064a\u0627\u062a \u0627\u0644\u0645\u0648\u0627\u0642\u0639 \u0627\u0644\u0645\u062d\u0641\u0648\u0638\u0629 \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0643", + "location_search_provider_title": "\u0645\u0648\u0641\u0631 \u0627\u0644\u0628\u062d\u062b \u0639\u0646 \u0627\u0644\u0645\u0648\u0642\u0639", + "google_maps_api_key_title": "\u0645\u0641\u062a\u0627\u062d Google Maps API", + "option_osm": "OpenStreetMap (\u0645\u062c\u0627\u0646\u064a)", + "option_google_maps": "\u062e\u0631\u0627\u0626\u0637 Google" } }, "dialogs": { "add_friend": { - "title": "إضافة صديق أو مجموعة", - "search_hint": "بحث", - "fetch_error": "فشل جلب البيانات", - "category_groups": "المجموعات", - "category_friends": "الأصدقاء", - "participants_text": "{count} مشارك", - "unselect_all_button": "إلغاء تحديد الكل" + "title": "\u0625\u0636\u0627\u0641\u0629 \u0635\u062f\u064a\u0642 \u0623\u0648 \u0645\u062c\u0645\u0648\u0639\u0629", + "search_hint": "\u0628\u062d\u062b", + "fetch_error": "\u0641\u0634\u0644 \u062c\u0644\u0628 \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a", + "category_groups": "\u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0627\u062a", + "category_friends": "\u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "participants_text": "{count} \u0645\u0634\u0627\u0631\u0643", + "unselect_all_button": "\u0625\u0644\u063a\u0627\u0621 \u062a\u062d\u062f\u064a\u062f \u0627\u0644\u0643\u0644" }, "scripting": { - "repo_hint": "الصق رابط المستودع" + "repo_hint": "\u0627\u0644\u0635\u0642 \u0631\u0627\u0628\u0637 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639" }, "scripting_warning": { - "title": "تحذير", - "content": "يتضمن PurrfectSnap أداة برمجة، مما يسمح بتنفيذ كود معرف من قبل المستخدم على جهازك. استخدم أقصى درجات الحذر وقم بتثبيت الوحدات فقط من مصادر معروفة وموثوقة. قد تشكل الوحدات غير المصرح بها أو غير التي تم التحقق منها مخاطر أمنية على نظامك." + "title": "\u062a\u062d\u0630\u064a\u0631", + "content": "\u064a\u062a\u0636\u0645\u0646 PurrfectSnap \u0623\u062f\u0627\u0629 \u0628\u0631\u0645\u062c\u0629\u060c \u0645\u0645\u0627 \u064a\u0633\u0645\u062d \u0628\u062a\u0646\u0641\u064a\u0630 \u0643\u0648\u062f \u0645\u0639\u0631\u0641 \u0645\u0646 \u0642\u0628\u0644 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0639\u0644\u0649 \u062c\u0647\u0627\u0632\u0643. \u0627\u0633\u062a\u062e\u062f\u0645 \u0623\u0642\u0635\u0649 \u062f\u0631\u062c\u0627\u062a \u0627\u0644\u062d\u0630\u0631 \u0648\u0642\u0645 \u0628\u062a\u062b\u0628\u064a\u062a \u0627\u0644\u0648\u062d\u062f\u0627\u062a \u0641\u0642\u0637 \u0645\u0646 \u0645\u0635\u0627\u062f\u0631 \u0645\u0639\u0631\u0648\u0641\u0629 \u0648\u0645\u0648\u062b\u0648\u0642\u0629. \u0642\u062f \u062a\u0634\u0643\u0644 \u0627\u0644\u0648\u062d\u062f\u0627\u062a \u063a\u064a\u0631 \u0627\u0644\u0645\u0635\u0631\u062d \u0628\u0647\u0627 \u0623\u0648 \u063a\u064a\u0631 \u0627\u0644\u062a\u064a \u062a\u0645 \u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0646\u0647\u0627 \u0645\u062e\u0627\u0637\u0631 \u0623\u0645\u0646\u064a\u0629 \u0639\u0644\u0649 \u0646\u0638\u0627\u0645\u0643." }, "reset_config": { - "title": "إعادة تعيين التكوين", - "content": "هل أنت متأكد أنك تريد إعادة تعيين التكوين؟", - "success_toast": "تمت إعادة تعيين التكوين بنجاح" + "title": "\u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646 \u0627\u0644\u062a\u0643\u0648\u064a\u0646", + "content": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646 \u0627\u0644\u062a\u0643\u0648\u064a\u0646\u061f", + "success_toast": "\u062a\u0645\u062a \u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646 \u0627\u0644\u062a\u0643\u0648\u064a\u0646 \u0628\u0646\u062c\u0627\u062d" }, "quick_actions_dialog": { - "title": "إجراءات سريعة", - "subtitle": "الوصول إلى أدواتك المفضلة بشكل أسرع" + "title": "\u0625\u062c\u0631\u0627\u0621\u0627\u062a \u0633\u0631\u064a\u0639\u0629", + "subtitle": "\u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0623\u062f\u0648\u0627\u062a\u0643 \u0627\u0644\u0645\u0641\u0636\u0644\u0629 \u0628\u0634\u0643\u0644 \u0623\u0633\u0631\u0639" }, "export_config": { - "title": "تصدير بيانات حساسة؟", - "content": "هل تريد تصدير التكوين مع البيانات الحساسة؟ (مثل إحداثيات الموقع، إلخ)" + "title": "\u062a\u0635\u062f\u064a\u0631 \u0628\u064a\u0627\u0646\u0627\u062a \u062d\u0633\u0627\u0633\u0629\u061f", + "content": "\u0647\u0644 \u062a\u0631\u064a\u062f \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u062a\u0643\u0648\u064a\u0646 \u0645\u0639 \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u062d\u0633\u0627\u0633\u0629\u061f (\u0645\u062b\u0644 \u0625\u062d\u062f\u0627\u062b\u064a\u0627\u062a \u0627\u0644\u0645\u0648\u0642\u0639\u060c \u0625\u0644\u062e)" }, "messaging_action": { - "title": "اختر أنواع المحتوى للمعالجة", - "select_all_button": "تحديد الكل" + "title": "\u0627\u062e\u062a\u0631 \u0623\u0646\u0648\u0627\u0639 \u0627\u0644\u0645\u062d\u062a\u0648\u0649 \u0644\u0644\u0645\u0639\u0627\u0644\u062c\u0629", + "select_all_button": "\u062a\u062d\u062f\u064a\u062f \u0627\u0644\u0643\u0644" }, "file_imports": { - "no_files_settings_hint": "لم يتم العثور على ملفات. تأكد من أنك قمت باستيراد الملفات المطلوبة في قسم استيراد الملفات", - "settings_select_file_hint": "حدد ملفاً مستورداً", - "settings_select_file_subtitle": "اختر ملفاً من قائمة الملفات المستوردة" + "no_files_settings_hint": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0645\u0644\u0641\u0627\u062a. \u062a\u0623\u0643\u062f \u0645\u0646 \u0623\u0646\u0643 \u0642\u0645\u062a \u0628\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0645\u0644\u0641\u0627\u062a \u0627\u0644\u0645\u0637\u0644\u0648\u0628\u0629 \u0641\u064a \u0642\u0633\u0645 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0645\u0644\u0641\u0627\u062a", + "settings_select_file_hint": "\u062d\u062f\u062f \u0645\u0644\u0641\u0627\u064b \u0645\u0633\u062a\u0648\u0631\u062f\u0627\u064b", + "settings_select_file_subtitle": "\u0627\u062e\u062a\u0631 \u0645\u0644\u0641\u0627\u064b \u0645\u0646 \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0645\u0644\u0641\u0627\u062a \u0627\u0644\u0645\u0633\u062a\u0648\u0631\u062f\u0629" } }, "scripting": { - "actions_button": "الإجراءات", - "actions_title": "الإجراءات", - "catalog_tab": "الكتالوج", - "clear_module_data_button": "مسح البيانات", - "clear_module_data_failed": "فشل مسح بيانات الوحدة", - "delete_module_button": "حذف", - "delete_module_failed": "فشل حذف الوحدة", - "documentation_button": "وثائق", - "download_script_failed": "فشل تنزيل السكربت", - "downloading_script": "جاري تنزيل السكربت...", - "edit_module_button": "تعديل", - "enter_url_label": "أدخل الرابط", - "import_button": "استيراد", - "import_from_url_button": "استيراد من رابط", - "import_script_from_url_title": "استيراد سكربت من رابط", - "import_failed": "فشل الاستيراد: {message}", - "import_script_warning": "قم بتثبيت السكربتات فقط من المصادر التي تثق بها.", - "installed_scripts_tab": "المثبتة", - "manage_repos_button": "إدارة المستودعات", - "module_data_cleared": "تم مسح بيانات الوحدة!", - "module_not_found": "الوحدة غير موجودة", - "no_description": "لا يوجد وصف", - "no_scripts_folder_selected_title": "حدد مجلد السكربتات للبدء", - "no_scripts_found_title": "لم يتم العثور على سكربتات", - "no_settings_for_module": "هذه الوحدة ليس لديها أي إعدادات", - "open_module_failed": "فشل فتح ملف الوحدة", - "open_scripts_folder_button": "فتح مجلد السكربتات", - "loaded_script": "تم تحميل السكربت {name}", - "unloaded_script": "تم إلغاء تحميل السكربت {name}", - "script_already_installed": "السكربت مثبت بالفعل", - "select_folder_button": "اختر المجلد", - "select_scripts_folder_toast": "يرجى اختيار مجلد السكربتات أولاً", - "update_module_button": "تحديث الوحدة", - "update_module_failed": "فشل تحديث الوحدة", - "use_catalog_to_add_scripts": "استخدم الكتالوج لإضافة سكربتات", - "ok_button_timeout": "موافق {timeout}", + "actions_button": "\u0627\u0644\u0625\u062c\u0631\u0627\u0621\u0627\u062a", + "actions_title": "\u0627\u0644\u0625\u062c\u0631\u0627\u0621\u0627\u062a", + "catalog_tab": "\u0627\u0644\u0643\u062a\u0627\u0644\u0648\u062c", + "clear_module_data_button": "\u0645\u0633\u062d \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a", + "clear_module_data_failed": "\u0641\u0634\u0644 \u0645\u0633\u062d \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0648\u062d\u062f\u0629", + "delete_module_button": "\u062d\u0630\u0641", + "delete_module_failed": "\u0641\u0634\u0644 \u062d\u0630\u0641 \u0627\u0644\u0648\u062d\u062f\u0629", + "documentation_button": "\u0648\u062b\u0627\u0626\u0642", + "download_script_failed": "\u0641\u0634\u0644 \u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0633\u0643\u0631\u0628\u062a", + "downloading_script": "\u062c\u0627\u0631\u064a \u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0633\u0643\u0631\u0628\u062a...", + "edit_module_button": "\u062a\u0639\u062f\u064a\u0644", + "enter_url_label": "\u0623\u062f\u062e\u0644 \u0627\u0644\u0631\u0627\u0628\u0637", + "import_button": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f", + "import_from_url_button": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0645\u0646 \u0631\u0627\u0628\u0637", + "import_script_from_url_title": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0633\u0643\u0631\u0628\u062a \u0645\u0646 \u0631\u0627\u0628\u0637", + "import_failed": "\u0641\u0634\u0644 \u0627\u0644\u0627\u0633\u062a\u064a\u0631\u0627\u062f: {message}", + "import_script_warning": "\u0642\u0645 \u0628\u062a\u062b\u0628\u064a\u062a \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a \u0641\u0642\u0637 \u0645\u0646 \u0627\u0644\u0645\u0635\u0627\u062f\u0631 \u0627\u0644\u062a\u064a \u062a\u062b\u0642 \u0628\u0647\u0627.", + "installed_scripts_tab": "\u0627\u0644\u0645\u062b\u0628\u062a\u0629", + "manage_repos_button": "\u0625\u062f\u0627\u0631\u0629 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639\u0627\u062a", + "module_data_cleared": "\u062a\u0645 \u0645\u0633\u062d \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0648\u062d\u062f\u0629!", + "module_not_found": "\u0627\u0644\u0648\u062d\u062f\u0629 \u063a\u064a\u0631 \u0645\u0648\u062c\u0648\u062f\u0629", + "no_description": "\u0644\u0627 \u064a\u0648\u062c\u062f \u0648\u0635\u0641", + "no_scripts_folder_selected_title": "\u062d\u062f\u062f \u0645\u062c\u0644\u062f \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a \u0644\u0644\u0628\u062f\u0621", + "no_scripts_found_title": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0633\u0643\u0631\u0628\u062a\u0627\u062a", + "no_settings_for_module": "\u0647\u0630\u0647 \u0627\u0644\u0648\u062d\u062f\u0629 \u0644\u064a\u0633 \u0644\u062f\u064a\u0647\u0627 \u0623\u064a \u0625\u0639\u062f\u0627\u062f\u0627\u062a", + "open_module_failed": "\u0641\u0634\u0644 \u0641\u062a\u062d \u0645\u0644\u0641 \u0627\u0644\u0648\u062d\u062f\u0629", + "open_scripts_folder_button": "\u0641\u062a\u062d \u0645\u062c\u0644\u062f \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a", + "loaded_script": "\u062a\u0645 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0633\u0643\u0631\u0628\u062a {name}", + "unloaded_script": "\u062a\u0645 \u0625\u0644\u063a\u0627\u0621 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0633\u0643\u0631\u0628\u062a {name}", + "script_already_installed": "\u0627\u0644\u0633\u0643\u0631\u0628\u062a \u0645\u062b\u0628\u062a \u0628\u0627\u0644\u0641\u0639\u0644", + "select_folder_button": "\u0627\u062e\u062a\u0631 \u0627\u0644\u0645\u062c\u0644\u062f", + "select_scripts_folder_toast": "\u064a\u0631\u062c\u0649 \u0627\u062e\u062a\u064a\u0627\u0631 \u0645\u062c\u0644\u062f \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a \u0623\u0648\u0644\u0627\u064b", + "update_module_button": "\u062a\u062d\u062f\u064a\u062b \u0627\u0644\u0648\u062d\u062f\u0629", + "update_module_failed": "\u0641\u0634\u0644 \u062a\u062d\u062f\u064a\u062b \u0627\u0644\u0648\u062d\u062f\u0629", + "use_catalog_to_add_scripts": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u0643\u062a\u0627\u0644\u0648\u062c \u0644\u0625\u0636\u0627\u0641\u0629 \u0633\u0643\u0631\u0628\u062a\u0627\u062a", + "ok_button_timeout": "\u0645\u0648\u0627\u0641\u0642 {timeout}", "catalog": { - "no_repos_added": "لم تتم إضافة مستودعات", - "repo_list_info": "ابحث عن المستودعات هنا:", - "link_text": "قائمة المستودعات", - "loading": "جاري تحميل الكتالوج...", - "script_already_installed": "السكربت مثبت بالفعل", - "script_downloaded": "تم تنزيل السكربت", - "could_not_create_file": "لا يمكن إنشاء الملف", - "no_scripts_folder_selected": "حدد مجلد السكربتات أولاً", - "no_scripts_available": "لا توجد سكربتات متاحة", - "installed_button": "مثبت", - "download_button": "تنزيل" + "no_repos_added": "\u0644\u0645 \u062a\u062a\u0645 \u0625\u0636\u0627\u0641\u0629 \u0645\u0633\u062a\u0648\u062f\u0639\u0627\u062a", + "repo_list_info": "\u0627\u0628\u062d\u062b \u0639\u0646 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639\u0627\u062a \u0647\u0646\u0627:", + "link_text": "\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639\u0627\u062a", + "loading": "\u062c\u0627\u0631\u064a \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0643\u062a\u0627\u0644\u0648\u062c...", + "script_already_installed": "\u0627\u0644\u0633\u0643\u0631\u0628\u062a \u0645\u062b\u0628\u062a \u0628\u0627\u0644\u0641\u0639\u0644", + "script_downloaded": "\u062a\u0645 \u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0633\u0643\u0631\u0628\u062a", + "could_not_create_file": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u0625\u0646\u0634\u0627\u0621 \u0627\u0644\u0645\u0644\u0641", + "no_scripts_folder_selected": "\u062d\u062f\u062f \u0645\u062c\u0644\u062f \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a \u0623\u0648\u0644\u0627\u064b", + "no_scripts_available": "\u0644\u0627 \u062a\u0648\u062c\u062f \u0633\u0643\u0631\u0628\u062a\u0627\u062a \u0645\u062a\u0627\u062d\u0629", + "installed_button": "\u0645\u062b\u0628\u062a", + "download_button": "\u062a\u0646\u0632\u064a\u0644" }, "repos": { - "no_repos_added": "لم تتم إضافة مستودعات", - "add_repo_button": "إضافة مستودع", - "add_repo_dialog_title": "إضافة مستودع", - "repo_url_label": "رابط المستودع", - "add_button": "إضافة", - "invalid_repo_title": "مستودع غير صالح", - "invalid_repo_error": "هذا المستودع يفتقد إلى البيانات المطلوبة.", - "repo_added_toast": "تمت إضافة المستودع", - "add_repo_failed_toast": "فشل إضافة المستودع: {message}", - "remove_button": "إزالة", - "remove_repo_dialog_title": "إزالة المستودع", - "remove_repo_dialog_text": "هل أنت متأكد أنك تريد إزالة هذا المستودع؟" + "no_repos_added": "\u0644\u0645 \u062a\u062a\u0645 \u0625\u0636\u0627\u0641\u0629 \u0645\u0633\u062a\u0648\u062f\u0639\u0627\u062a", + "add_repo_button": "\u0625\u0636\u0627\u0641\u0629 \u0645\u0633\u062a\u0648\u062f\u0639", + "add_repo_dialog_title": "\u0625\u0636\u0627\u0641\u0629 \u0645\u0633\u062a\u0648\u062f\u0639", + "repo_url_label": "\u0631\u0627\u0628\u0637 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639", + "add_button": "\u0625\u0636\u0627\u0641\u0629", + "invalid_repo_title": "\u0645\u0633\u062a\u0648\u062f\u0639 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d", + "invalid_repo_error": "\u0647\u0630\u0627 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639 \u064a\u0641\u062a\u0642\u062f \u0625\u0644\u0649 \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0645\u0637\u0644\u0648\u0628\u0629.", + "repo_added_toast": "\u062a\u0645\u062a \u0625\u0636\u0627\u0641\u0629 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639", + "add_repo_failed_toast": "\u0641\u0634\u0644 \u0625\u0636\u0627\u0641\u0629 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639: {message}", + "remove_button": "\u0625\u0632\u0627\u0644\u0629", + "remove_repo_dialog_title": "\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639", + "remove_repo_dialog_text": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u0625\u0632\u0627\u0644\u0629 \u0647\u0630\u0627 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639\u061f" } }, "friend_tracker": { - "rules_tab": "القواعد", - "logs_tab": "السجلات", - "catalog_button": "الكتالوج", - "add_rule_button": "إضافة قاعدة", - "import_button": "استيراد", - "filters_title": "فلاتر", - "search_by_label": "البحث بواسطة", - "newest_first_label": "الأحدث أولاً", - "since_label": "منذ", - "until_label": "حتى", - "unit_label": "الوحدة", - "pick_a_date_button": "اختر تاريخاً", - "export_button": "تصدير", - "delete_button": "حذف", - "search_placeholder": "بحث", - "no_logs_found": "لم يتم العثور على سجلات", - "no_rules_found": "لم يتم العثور على قواعد", - "export_logs_dialog_title": "تصدير السجلات", - "export_logs_dialog_confirm_text": "تصدير السجلات باستخدام الفلاتر الحالية؟", - "export_as_button": "تصدير كـ {type}", - "new_rule_title": "قاعدة جديدة", - "edit_rule_title": "تعديل القاعدة", - "general_section_title": "عام", - "rule_name_label": "اسم القاعدة", - "default_rule_name": "قاعدة جديدة", - "author_name_label": "المؤلف", - "scope_section_title": "النطاق", - "scope_all": "جميع الأصدقاء/المجموعات", - "scope_whitelist": "لا أحد باستثناء", - "scope_blacklist": "الجميع باستثناء", - "events_section_title": "الأحداث", - "events_suffix": "أحداث", - "scopes_suffix": "نطاقات", - "no_events_text": "لم تتم إضافة أحداث بعد", - "add_event_dialog_title": "إضافة حدث", - "event_type_label": "نوع الحدث", - "triggers_title": "المحفزات", - "conditions_title": "الشروط", - "condition_only_inside_conversation": "فقط عندما أكون داخل المحادثة", - "condition_only_outside_conversation": "فقط عندما أكون خارج المحادثة", - "condition_only_when_app_active": "فقط عندما يكون Snapchat نشطاً", - "condition_only_when_app_inactive": "فقط عندما يكون Snapchat غير نشط", - "condition_no_push_notification_when_app_active": "لا يوجد إشعار عندما يكون Snapchat نشطاً", - "add_button": "إضافة", - "cannot_save_rule_dialog_title": "لا يمكن حفظ القاعدة", - "cannot_save_rule_dialog_text": "املأ الحقول المفقودة لحفظ هذه القاعدة.", - "duplicate_rule_name_dialog_title": "اسم قاعدة مكرر", - "duplicate_rule_name_dialog_text": "توجد قاعدة بهذا الاسم بالفعل. اختر اسماً جديداً.", - "discard_changes_dialog_title": "تجاهل التغييرات؟", - "discard_changes_dialog_text": "لديك تغييرات غير محفوظة. تجاهلها؟", - "rule_subtitle": "تكوين المحفزات والنطاقات لهذه القاعدة.", - "discard_button": "تجاهل", - "enabled_label": "مفعل", - "disabled_label": "معطل", - "delete_rule_dialog_title": "حذف القاعدة", - "delete_rule_dialog_text": "هل أنت متأكد أنك تريد حذف هذه القاعدة؟", - "no_repos_added": "لم تتم إضافة مستودعات", - "import_dialog_title": "استيراد القواعد", - "read_file_failed_toast": "فشل قراءة الملف: {message}", - "bulk_import_button": "استيراد مجمع", - "individual_import_button": "استيراد فردي", - "invalid_import_type_dialog_title": "استيراد غير صالح", - "invalid_import_type_dialog_text": "نوع الملف المحدد لا يطابق وضع الاستيراد.", - "export_dialog_title": "تصدير القواعد", - "bulk_export_button": "تصدير مجمع", - "individual_export_button": "تصدير فردي", - "reverse_order_checkbox": "عكس الترتيب", - "delete_logs_dialog_title": "حذف السجلات", - "delete_logs_dialog_confirm_text": "حذف جميع السجلات التي تطابق الفلاتر الحالية؟", - "select_friends_groups_button": "تحديد أصدقاء / مجموعات" + "rules_tab": "\u0627\u0644\u0642\u0648\u0627\u0639\u062f", + "logs_tab": "\u0627\u0644\u0633\u062c\u0644\u0627\u062a", + "catalog_button": "\u0627\u0644\u0643\u062a\u0627\u0644\u0648\u062c", + "add_rule_button": "\u0625\u0636\u0627\u0641\u0629 \u0642\u0627\u0639\u062f\u0629", + "import_button": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f", + "filters_title": "\u0641\u0644\u0627\u062a\u0631", + "search_by_label": "\u0627\u0644\u0628\u062d\u062b \u0628\u0648\u0627\u0633\u0637\u0629", + "newest_first_label": "\u0627\u0644\u0623\u062d\u062f\u062b \u0623\u0648\u0644\u0627\u064b", + "since_label": "\u0645\u0646\u0630", + "until_label": "\u062d\u062a\u0649", + "unit_label": "\u0627\u0644\u0648\u062d\u062f\u0629", + "pick_a_date_button": "\u0627\u062e\u062a\u0631 \u062a\u0627\u0631\u064a\u062e\u0627\u064b", + "export_button": "\u062a\u0635\u062f\u064a\u0631", + "delete_button": "\u062d\u0630\u0641", + "search_placeholder": "\u0628\u062d\u062b", + "no_logs_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0633\u062c\u0644\u0627\u062a", + "no_rules_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0642\u0648\u0627\u0639\u062f", + "export_logs_dialog_title": "\u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0633\u062c\u0644\u0627\u062a", + "export_logs_dialog_confirm_text": "\u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u0641\u0644\u0627\u062a\u0631 \u0627\u0644\u062d\u0627\u0644\u064a\u0629\u061f", + "export_as_button": "\u062a\u0635\u062f\u064a\u0631 \u0643\u0640 {type}", + "new_rule_title": "\u0642\u0627\u0639\u062f\u0629 \u062c\u062f\u064a\u062f\u0629", + "edit_rule_title": "\u062a\u0639\u062f\u064a\u0644 \u0627\u0644\u0642\u0627\u0639\u062f\u0629", + "general_section_title": "\u0639\u0627\u0645", + "rule_name_label": "\u0627\u0633\u0645 \u0627\u0644\u0642\u0627\u0639\u062f\u0629", + "default_rule_name": "\u0642\u0627\u0639\u062f\u0629 \u062c\u062f\u064a\u062f\u0629", + "author_name_label": "\u0627\u0644\u0645\u0624\u0644\u0641", + "scope_section_title": "\u0627\u0644\u0646\u0637\u0627\u0642", + "scope_all": "\u062c\u0645\u064a\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621/\u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0627\u062a", + "scope_whitelist": "\u0644\u0627 \u0623\u062d\u062f \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621", + "scope_blacklist": "\u0627\u0644\u062c\u0645\u064a\u0639 \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621", + "events_section_title": "\u0627\u0644\u0623\u062d\u062f\u0627\u062b", + "events_suffix": "\u0623\u062d\u062f\u0627\u062b", + "scopes_suffix": "\u0646\u0637\u0627\u0642\u0627\u062a", + "no_events_text": "\u0644\u0645 \u062a\u062a\u0645 \u0625\u0636\u0627\u0641\u0629 \u0623\u062d\u062f\u0627\u062b \u0628\u0639\u062f", + "add_event_dialog_title": "\u0625\u0636\u0627\u0641\u0629 \u062d\u062f\u062b", + "event_type_label": "\u0646\u0648\u0639 \u0627\u0644\u062d\u062f\u062b", + "triggers_title": "\u0627\u0644\u0645\u062d\u0641\u0632\u0627\u062a", + "conditions_title": "\u0627\u0644\u0634\u0631\u0648\u0637", + "condition_only_inside_conversation": "\u0641\u0642\u0637 \u0639\u0646\u062f\u0645\u0627 \u0623\u0643\u0648\u0646 \u062f\u0627\u062e\u0644 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "condition_only_outside_conversation": "\u0641\u0642\u0637 \u0639\u0646\u062f\u0645\u0627 \u0623\u0643\u0648\u0646 \u062e\u0627\u0631\u062c \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "condition_only_when_app_active": "\u0641\u0642\u0637 \u0639\u0646\u062f\u0645\u0627 \u064a\u0643\u0648\u0646 Snapchat \u0646\u0634\u0637\u0627\u064b", + "condition_only_when_app_inactive": "\u0641\u0642\u0637 \u0639\u0646\u062f\u0645\u0627 \u064a\u0643\u0648\u0646 Snapchat \u063a\u064a\u0631 \u0646\u0634\u0637", + "condition_no_push_notification_when_app_active": "\u0644\u0627 \u064a\u0648\u062c\u062f \u0625\u0634\u0639\u0627\u0631 \u0639\u0646\u062f\u0645\u0627 \u064a\u0643\u0648\u0646 Snapchat \u0646\u0634\u0637\u0627\u064b", + "add_button": "\u0625\u0636\u0627\u0641\u0629", + "cannot_save_rule_dialog_title": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062d\u0641\u0638 \u0627\u0644\u0642\u0627\u0639\u062f\u0629", + "cannot_save_rule_dialog_text": "\u0627\u0645\u0644\u0623 \u0627\u0644\u062d\u0642\u0648\u0644 \u0627\u0644\u0645\u0641\u0642\u0648\u062f\u0629 \u0644\u062d\u0641\u0638 \u0647\u0630\u0647 \u0627\u0644\u0642\u0627\u0639\u062f\u0629.", + "duplicate_rule_name_dialog_title": "\u0627\u0633\u0645 \u0642\u0627\u0639\u062f\u0629 \u0645\u0643\u0631\u0631", + "duplicate_rule_name_dialog_text": "\u062a\u0648\u062c\u062f \u0642\u0627\u0639\u062f\u0629 \u0628\u0647\u0630\u0627 \u0627\u0644\u0627\u0633\u0645 \u0628\u0627\u0644\u0641\u0639\u0644. \u0627\u062e\u062a\u0631 \u0627\u0633\u0645\u0627\u064b \u062c\u062f\u064a\u062f\u0627\u064b.", + "discard_changes_dialog_title": "\u062a\u062c\u0627\u0647\u0644 \u0627\u0644\u062a\u063a\u064a\u064a\u0631\u0627\u062a\u061f", + "discard_changes_dialog_text": "\u0644\u062f\u064a\u0643 \u062a\u063a\u064a\u064a\u0631\u0627\u062a \u063a\u064a\u0631 \u0645\u062d\u0641\u0648\u0638\u0629. \u062a\u062c\u0627\u0647\u0644\u0647\u0627\u061f", + "rule_subtitle": "\u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u0645\u062d\u0641\u0632\u0627\u062a \u0648\u0627\u0644\u0646\u0637\u0627\u0642\u0627\u062a \u0644\u0647\u0630\u0647 \u0627\u0644\u0642\u0627\u0639\u062f\u0629.", + "discard_button": "\u062a\u062c\u0627\u0647\u0644", + "enabled_label": "\u0645\u0641\u0639\u0644", + "disabled_label": "\u0645\u0639\u0637\u0644", + "delete_rule_dialog_title": "\u062d\u0630\u0641 \u0627\u0644\u0642\u0627\u0639\u062f\u0629", + "delete_rule_dialog_text": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u062d\u0630\u0641 \u0647\u0630\u0647 \u0627\u0644\u0642\u0627\u0639\u062f\u0629\u061f", + "no_repos_added": "\u0644\u0645 \u062a\u062a\u0645 \u0625\u0636\u0627\u0641\u0629 \u0645\u0633\u062a\u0648\u062f\u0639\u0627\u062a", + "import_dialog_title": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0642\u0648\u0627\u0639\u062f", + "read_file_failed_toast": "\u0641\u0634\u0644 \u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0645\u0644\u0641: {message}", + "bulk_import_button": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0645\u062c\u0645\u0639", + "individual_import_button": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0641\u0631\u062f\u064a", + "invalid_import_type_dialog_title": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u063a\u064a\u0631 \u0635\u0627\u0644\u062d", + "invalid_import_type_dialog_text": "\u0646\u0648\u0639 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0645\u062d\u062f\u062f \u0644\u0627 \u064a\u0637\u0627\u0628\u0642 \u0648\u0636\u0639 \u0627\u0644\u0627\u0633\u062a\u064a\u0631\u0627\u062f.", + "export_dialog_title": "\u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0642\u0648\u0627\u0639\u062f", + "bulk_export_button": "\u062a\u0635\u062f\u064a\u0631 \u0645\u062c\u0645\u0639", + "individual_export_button": "\u062a\u0635\u062f\u064a\u0631 \u0641\u0631\u062f\u064a", + "reverse_order_checkbox": "\u0639\u0643\u0633 \u0627\u0644\u062a\u0631\u062a\u064a\u0628", + "delete_logs_dialog_title": "\u062d\u0630\u0641 \u0627\u0644\u0633\u062c\u0644\u0627\u062a", + "delete_logs_dialog_confirm_text": "\u062d\u0630\u0641 \u062c\u0645\u064a\u0639 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0627\u0644\u062a\u064a \u062a\u0637\u0627\u0628\u0642 \u0627\u0644\u0641\u0644\u0627\u062a\u0631 \u0627\u0644\u062d\u0627\u0644\u064a\u0629\u061f", + "select_friends_groups_button": "\u062a\u062d\u062f\u064a\u062f \u0623\u0635\u062f\u0642\u0627\u0621 / \u0645\u062c\u0645\u0648\u0639\u0627\u062a" }, "friend_tracker_export": { - "title": "تصدير متتبع الأصدقاء", - "save_button": "حفظ", - "back_button_description": "رجوع", - "expand_button_description": "توسيع أو طي الفئة", - "tracker_author_label": "المؤلف", - "tracker_enabled_label": "مفعل", - "tracker_enabled_value": "مفعل", - "tracker_disabled_value": "معطل", - "exported_toast": "تم تصدير تكوين المتتبع", - "export_failed_toast": "فشل تصدير المتتبع: {message}" + "title": "\u062a\u0635\u062f\u064a\u0631 \u0645\u062a\u062a\u0628\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "save_button": "\u062d\u0641\u0638", + "back_button_description": "\u0631\u062c\u0648\u0639", + "expand_button_description": "\u062a\u0648\u0633\u064a\u0639 \u0623\u0648 \u0637\u064a \u0627\u0644\u0641\u0626\u0629", + "tracker_author_label": "\u0627\u0644\u0645\u0624\u0644\u0641", + "tracker_enabled_label": "\u0645\u0641\u0639\u0644", + "tracker_enabled_value": "\u0645\u0641\u0639\u0644", + "tracker_disabled_value": "\u0645\u0639\u0637\u0644", + "exported_toast": "\u062a\u0645 \u062a\u0635\u062f\u064a\u0631 \u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u0645\u062a\u062a\u0628\u0639", + "export_failed_toast": "\u0641\u0634\u0644 \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0645\u062a\u062a\u0628\u0639: {message}" }, "friend_tracker_import": { - "title": "استيراد متتبع الأصدقاء", - "confirm_button": "استيراد", - "back_button_description": "رجوع", - "expand_button_description": "توسيع أو طي الفئة", - "tracker_author_label": "المؤلف", - "tracker_enabled_label": "مفعل", - "tracker_enabled_value": "مفعل", - "tracker_disabled_value": "معطل", - "imported_toast": "تم استيراد المتتبع", - "import_failed_toast": "فشل استيراد المتتبع: {message}" + "title": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0645\u062a\u062a\u0628\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "confirm_button": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f", + "back_button_description": "\u0631\u062c\u0648\u0639", + "expand_button_description": "\u062a\u0648\u0633\u064a\u0639 \u0623\u0648 \u0637\u064a \u0627\u0644\u0641\u0626\u0629", + "tracker_author_label": "\u0627\u0644\u0645\u0624\u0644\u0641", + "tracker_enabled_label": "\u0645\u0641\u0639\u0644", + "tracker_enabled_value": "\u0645\u0641\u0639\u0644", + "tracker_disabled_value": "\u0645\u0639\u0637\u0644", + "imported_toast": "\u062a\u0645 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0645\u062a\u062a\u0628\u0639", + "import_failed_toast": "\u0641\u0634\u0644 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0645\u062a\u062a\u0628\u0639: {message}" }, "friend_tracker_catalog": { - "title": "كتالوج متتبع الأصدقاء", - "no_repos_added": "لم تتم إضافة مستودعات", - "manage_repos_description": "إدارة المستودعات" + "title": "\u0643\u062a\u0627\u0644\u0648\u062c \u0645\u062a\u062a\u0628\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "no_repos_added": "\u0644\u0645 \u062a\u062a\u0645 \u0625\u0636\u0627\u0641\u0629 \u0645\u0633\u062a\u0648\u062f\u0639\u0627\u062a", + "manage_repos_description": "\u0625\u062f\u0627\u0631\u0629 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639\u0627\u062a" }, "friend_tracker_repos": { - "no_repos_added": "لم تتم إضافة مستودعات", - "add_repo_button": "إضافة مستودع", - "add_repo_dialog_title": "إضافة مستودع", - "repo_url_label": "رابط المستودع", - "add_button": "إضافة", - "invalid_repo_title": "مستودع غير صالح", - "invalid_repo_error": "هذا المستودع يفتقد إلى البيانات المطلوبة.", - "repo_added_toast": "تمت إضافة المستودع", - "add_repo_failed_toast": "فشل إضافة المستودع: {message}", - "remove_button": "إزالة", - "remove_repo_dialog_title": "إزالة المستودع", - "remove_repo_dialog_text": "هل أنت متأكد أنك تريد إزالة هذا المستودع؟" + "no_repos_added": "\u0644\u0645 \u062a\u062a\u0645 \u0625\u0636\u0627\u0641\u0629 \u0645\u0633\u062a\u0648\u062f\u0639\u0627\u062a", + "add_repo_button": "\u0625\u0636\u0627\u0641\u0629 \u0645\u0633\u062a\u0648\u062f\u0639", + "add_repo_dialog_title": "\u0625\u0636\u0627\u0641\u0629 \u0645\u0633\u062a\u0648\u062f\u0639", + "repo_url_label": "\u0631\u0627\u0628\u0637 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639", + "add_button": "\u0625\u0636\u0627\u0641\u0629", + "invalid_repo_title": "\u0645\u0633\u062a\u0648\u062f\u0639 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d", + "invalid_repo_error": "\u0647\u0630\u0627 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639 \u064a\u0641\u062a\u0642\u062f \u0625\u0644\u0649 \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0645\u0637\u0644\u0648\u0628\u0629.", + "repo_added_toast": "\u062a\u0645\u062a \u0625\u0636\u0627\u0641\u0629 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639", + "add_repo_failed_toast": "\u0641\u0634\u0644 \u0625\u0636\u0627\u0641\u0629 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639: {message}", + "remove_button": "\u0625\u0632\u0627\u0644\u0629", + "remove_repo_dialog_title": "\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639", + "remove_repo_dialog_text": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u0625\u0632\u0627\u0644\u0629 \u0647\u0630\u0627 \u0627\u0644\u0645\u0633\u062a\u0648\u062f\u0639\u061f" }, "logger_history": { - "select_conversation_placeholder": "حدد محادثة" + "select_conversation_placeholder": "\u062d\u062f\u062f \u0645\u062d\u0627\u062f\u062b\u0629" }, "features": { "config_export": { - "title": "تصدير ملخص التكوين", - "back_button_description": "رجوع", - "save_button": "حفظ", - "expand_button_description": "توسيع أو طي الفئة", - "enabled": "مفعل", - "disabled": "معطل", - "enable_feature": "تمكين الميزة" + "title": "\u062a\u0635\u062f\u064a\u0631 \u0645\u0644\u062e\u0635 \u0627\u0644\u062a\u0643\u0648\u064a\u0646", + "back_button_description": "\u0631\u062c\u0648\u0639", + "save_button": "\u062d\u0641\u0638", + "expand_button_description": "\u062a\u0648\u0633\u064a\u0639 \u0623\u0648 \u0637\u064a \u0627\u0644\u0641\u0626\u0629", + "enabled": "\u0645\u0641\u0639\u0644", + "disabled": "\u0645\u0639\u0637\u0644", + "enable_feature": "\u062a\u0645\u0643\u064a\u0646 \u0627\u0644\u0645\u064a\u0632\u0629" }, "config_import": { - "title": "استيراد ملخص التكوين", - "back_button_description": "رجوع", - "confirm_button": "استيراد", - "expand_button_description": "توسيع أو طي الفئة", - "enabled": "مفعل", - "disabled": "معطل", - "enable_feature": "تمكين الميزة", - "config_imported_toast": "تم استيراد التكوين بنجاح", - "config_import_failure_toast": "فشل استيراد التكوين {error}" + "title": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0645\u0644\u062e\u0635 \u0627\u0644\u062a\u0643\u0648\u064a\u0646", + "back_button_description": "\u0631\u062c\u0648\u0639", + "confirm_button": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f", + "expand_button_description": "\u062a\u0648\u0633\u064a\u0639 \u0623\u0648 \u0637\u064a \u0627\u0644\u0641\u0626\u0629", + "enabled": "\u0645\u0641\u0639\u0644", + "disabled": "\u0645\u0639\u0637\u0644", + "enable_feature": "\u062a\u0645\u0643\u064a\u0646 \u0627\u0644\u0645\u064a\u0632\u0629", + "config_imported_toast": "\u062a\u0645 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u062a\u0643\u0648\u064a\u0646 \u0628\u0646\u062c\u0627\u062d", + "config_import_failure_toast": "\u0641\u0634\u0644 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u062a\u0643\u0648\u064a\u0646 {error}" } } }, "rules": { "toasts": { - "enabled": "{ruleName} مفعل", - "disabled": "{ruleName} معطل" + "enabled": "{ruleName} \u0645\u0641\u0639\u0644", + "disabled": "{ruleName} \u0645\u0639\u0637\u0644" }, "modes": { - "blacklist": "وضع القائمة السوداء", - "whitelist": "وضع القائمة البيضاء" + "blacklist": "\u0648\u0636\u0639 \u0627\u0644\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0633\u0648\u062f\u0627\u0621", + "whitelist": "\u0648\u0636\u0639 \u0627\u0644\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0628\u064a\u0636\u0627\u0621" }, "properties": { "auto_download": { - "name": "تنزيل تلقائي", - "description": "تنزيل الـ Snaps تلقائياً عند عرضها", + "name": "\u062a\u0646\u0632\u064a\u0644 \u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0639\u0646\u062f \u0639\u0631\u0636\u0647\u0627", "options": { - "blacklist": "استبعاد من التنزيل التلقائي", - "whitelist": "تنزيل تلقائي" + "blacklist": "\u0627\u0633\u062a\u0628\u0639\u0627\u062f \u0645\u0646 \u0627\u0644\u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "whitelist": "\u062a\u0646\u0632\u064a\u0644 \u062a\u0644\u0642\u0627\u0626\u064a" } }, "stealth": { - "name": "وضع التخفي", - "description": "يمنع أي شخص من معرفة أنك فتحت الـ Snaps/المحادثات الخاصة بهم", + "name": "\u0648\u0636\u0639 \u0627\u0644\u062a\u062e\u0641\u064a", + "description": "\u064a\u0645\u0646\u0639 \u0623\u064a \u0634\u062e\u0635 \u0645\u0646 \u0645\u0639\u0631\u0641\u0629 \u0623\u0646\u0643 \u0641\u062a\u062d\u062a \u0627\u0644\u0640 Snaps/\u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0627\u062a \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0647\u0645", "options": { - "blacklist": "استبعاد من وضع التخفي", - "whitelist": "وضع التخفي" + "blacklist": "\u0627\u0633\u062a\u0628\u0639\u0627\u062f \u0645\u0646 \u0648\u0636\u0639 \u0627\u0644\u062a\u062e\u0641\u064a", + "whitelist": "\u0648\u0636\u0639 \u0627\u0644\u062a\u062e\u0641\u064a" } }, "auto_save": { - "name": "حفظ تلقائي", - "description": "يحفظ رسائل الدردشة عند عرضها", + "name": "\u062d\u0641\u0638 \u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u064a\u062d\u0641\u0638 \u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062f\u0631\u062f\u0634\u0629 \u0639\u0646\u062f \u0639\u0631\u0636\u0647\u0627", "options": { - "blacklist": "استبعاد من الحفظ التلقائي", - "whitelist": "حفظ تلقائي" + "blacklist": "\u0627\u0633\u062a\u0628\u0639\u0627\u062f \u0645\u0646 \u0627\u0644\u062d\u0641\u0638 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "whitelist": "\u062d\u0641\u0638 \u062a\u0644\u0642\u0627\u0626\u064a" } }, "unsaveable_messages": { - "name": "رسائل غير قابلة للحفظ", - "description": "يمنع حفظ الرسائل في الدردشة من قبل أشخاص آخرين", + "name": "\u0631\u0633\u0627\u0626\u0644 \u063a\u064a\u0631 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638", + "description": "\u064a\u0645\u0646\u0639 \u062d\u0641\u0638 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629 \u0645\u0646 \u0642\u0628\u0644 \u0623\u0634\u062e\u0627\u0635 \u0622\u062e\u0631\u064a\u0646", "options": { - "blacklist": "استبعاد من الرسائل غير القابلة للحفظ", - "whitelist": "رسائل غير قابلة للحفظ" + "blacklist": "\u0627\u0633\u062a\u0628\u0639\u0627\u062f \u0645\u0646 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u063a\u064a\u0631 \u0627\u0644\u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638", + "whitelist": "\u0631\u0633\u0627\u0626\u0644 \u063a\u064a\u0631 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638" } }, "auto_open_snaps": { - "name": "فتح الـ Snaps تلقائياً", - "description": "يفتح الـ Snaps تلقائياً عند استلامها", + "name": "\u0641\u062a\u062d \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "description": "\u064a\u0641\u062a\u062d \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0639\u0646\u062f \u0627\u0633\u062a\u0644\u0627\u0645\u0647\u0627", "options": { - "blacklist": "استبعاد من فتح الـ Snaps تلقائياً", - "whitelist": "فتح الـ Snaps تلقائياً" + "blacklist": "\u0627\u0633\u062a\u0628\u0639\u0627\u062f \u0645\u0646 \u0641\u062a\u062d \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "whitelist": "\u0641\u062a\u062d \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b" } }, "hide_friend_feed": { - "name": "إخفاء من موجز الأصدقاء" + "name": "\u0625\u062e\u0641\u0627\u0621 \u0645\u0646 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621" }, "e2e_encryption": { - "name": "استخدام التشفير من طرف لطرف" + "name": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u062a\u0634\u0641\u064a\u0631 \u0645\u0646 \u0637\u0631\u0641 \u0644\u0637\u0631\u0641" }, "pin_conversation": { - "name": "تثبيت المحادثة" + "name": "\u062a\u062b\u0628\u064a\u062a \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629" }, "exclude_message_logger": { - "name": "استبعاد من مسجل الرسائل" + "name": "\u0627\u0633\u062a\u0628\u0639\u0627\u062f \u0645\u0646 \u0645\u0633\u062c\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644" }, "auto_reply": { - "name": "الرد التلقائي", - "description": "يرسل ردوداً تلقائية على الرسائل الواردة عندما تكون بعيداً", + "name": "\u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u064a\u0631\u0633\u0644 \u0631\u062f\u0648\u062f\u0627\u064b \u062a\u0644\u0642\u0627\u0626\u064a\u0629 \u0639\u0644\u0649 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0648\u0627\u0631\u062f\u0629 \u0639\u0646\u062f\u0645\u0627 \u062a\u0643\u0648\u0646 \u0628\u0639\u064a\u062f\u0627\u064b", "options": { - "blacklist": "استبعاد من الرد التلقائي", - "whitelist": "الرد التلقائي" + "blacklist": "\u0627\u0633\u062a\u0628\u0639\u0627\u062f \u0645\u0646 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "whitelist": "\u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a" } }, "auto_delete_sent_messages": { - "name": "حذف الرسائل المرسلة تلقائياً", - "description": "يحذف الرسائل المرسلة تلقائياً بعد فترة زمنية محددة", + "name": "\u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0631\u0633\u0644\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "description": "\u064a\u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0631\u0633\u0644\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0628\u0639\u062f \u0641\u062a\u0631\u0629 \u0632\u0645\u0646\u064a\u0629 \u0645\u062d\u062f\u062f\u0629", "options": { - "blacklist": "استبعاد من حذف الرسائل المرسلة تلقائياً", - "whitelist": "حذف الرسائل المرسلة تلقائياً" + "blacklist": "\u0627\u0633\u062a\u0628\u0639\u0627\u062f \u0645\u0646 \u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0631\u0633\u0644\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "whitelist": "\u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0631\u0633\u0644\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b" } }, "message_logger": { - "name": "مسجل الرسائل", - "description": "الاحتفاظ بنسخة من الرسائل حتى إذا تم حذفها", + "name": "\u0645\u0633\u062c\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "description": "\u0627\u0644\u0627\u062d\u062a\u0641\u0627\u0638 \u0628\u0646\u0633\u062e\u0629 \u0645\u0646 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u062d\u062a\u0649 \u0625\u0630\u0627 \u062a\u0645 \u062d\u0630\u0641\u0647\u0627", "options": { - "blacklist": "استبعاد من مسجل الرسائل", - "whitelist": "مسجل الرسائل" + "blacklist": "\u0627\u0633\u062a\u0628\u0639\u0627\u062f \u0645\u0646 \u0645\u0633\u062c\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "whitelist": "\u0645\u0633\u062c\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644" } }, "auto_read": { - "name": "قراءة تلقائية", - "description": "وضع علامة \"مقروء\" تلقائياً على الـ snaps والدردشات", + "name": "\u0642\u0631\u0627\u0621\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0629", + "description": "\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u0645\u0642\u0631\u0648\u0621\" \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0639\u0644\u0649 \u0627\u0644\u0640 snaps \u0648\u0627\u0644\u062f\u0631\u062f\u0634\u0627\u062a", "options": { - "blacklist": "استبعاد من القراءة التلقائية", - "whitelist": "قراءة تلقائية" + "blacklist": "\u0627\u0633\u062a\u0628\u0639\u0627\u062f \u0645\u0646 \u0627\u0644\u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a\u0629", + "whitelist": "\u0642\u0631\u0627\u0621\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0629" } }, "hide_typing_indicator": { - "name": "إخفاء مؤشر الكتابة", - "description": "منع الآخرين من رؤية أنك تكتب", + "name": "\u0625\u062e\u0641\u0627\u0621 \u0645\u0624\u0634\u0631 \u0627\u0644\u0643\u062a\u0627\u0628\u0629", + "description": "\u0645\u0646\u0639 \u0627\u0644\u0622\u062e\u0631\u064a\u0646 \u0645\u0646 \u0631\u0624\u064a\u0629 \u0623\u0646\u0643 \u062a\u0643\u062a\u0628", "options": { - "blacklist": "استبعاد من إخفاء مؤشر الكتابة", - "whitelist": "إخفاء مؤشر الكتابة" + "blacklist": "\u0627\u0633\u062a\u0628\u0639\u0627\u062f \u0645\u0646 \u0625\u062e\u0641\u0627\u0621 \u0645\u0624\u0634\u0631 \u0627\u0644\u0643\u062a\u0627\u0628\u0629", + "whitelist": "\u0625\u062e\u0641\u0627\u0621 \u0645\u0624\u0634\u0631 \u0627\u0644\u0643\u062a\u0627\u0628\u0629" } } } }, "actions": { "clean_snapchat_cache": { - "name": "تنظيف ذاكرة التخزين المؤقت لـ Snapchat", - "description": "ينظف ذاكرة التخزين المؤقت لـ Snapchat" + "name": "\u062a\u0646\u0638\u064a\u0641 \u0630\u0627\u0643\u0631\u0629 \u0627\u0644\u062a\u062e\u0632\u064a\u0646 \u0627\u0644\u0645\u0624\u0642\u062a \u0644\u0640 Snapchat", + "description": "\u064a\u0646\u0638\u0641 \u0630\u0627\u0643\u0631\u0629 \u0627\u0644\u062a\u062e\u0632\u064a\u0646 \u0627\u0644\u0645\u0624\u0642\u062a \u0644\u0640 Snapchat" }, "manage_friend_list": { - "name": "إدارة قائمة الأصدقاء", - "description": "استيراد/تصدير قائمة أصدقائك عند النسخ الاحتياطي" + "name": "\u0625\u062f\u0627\u0631\u0629 \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "description": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f/\u062a\u0635\u062f\u064a\u0631 \u0642\u0627\u0626\u0645\u0629 \u0623\u0635\u062f\u0642\u0627\u0626\u0643 \u0639\u0646\u062f \u0627\u0644\u0646\u0633\u062e \u0627\u0644\u0627\u062d\u062a\u064a\u0627\u0637\u064a" }, "export_chat_messages": { - "name": "تصدير رسائل الدردشة", - "description": "تصدير رسائل المحادثة إلى ملف JSON/HTML/TXT" + "name": "\u062a\u0635\u062f\u064a\u0631 \u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "description": "\u062a\u0635\u062f\u064a\u0631 \u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629 \u0625\u0644\u0649 \u0645\u0644\u0641 JSON/HTML/TXT" }, "export_memories": { - "name": "تصدير الذكريات", - "description": "تصدير الذكريات إلى ملف ZIP" + "name": "\u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0630\u0643\u0631\u064a\u0627\u062a", + "description": "\u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0630\u0643\u0631\u064a\u0627\u062a \u0625\u0644\u0649 \u0645\u0644\u0641 ZIP" }, "bulk_messaging_action": { - "name": "إجراء المراسلة الجماعية", - "description": "ينفذ عمليات مثل حذف الأصدقاء أو الحذف الجماعي للمحادثات" + "name": "\u0625\u062c\u0631\u0627\u0621 \u0627\u0644\u0645\u0631\u0627\u0633\u0644\u0629 \u0627\u0644\u062c\u0645\u0627\u0639\u064a\u0629", + "description": "\u064a\u0646\u0641\u0630 \u0639\u0645\u0644\u064a\u0627\u062a \u0645\u062b\u0644 \u062d\u0630\u0641 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0623\u0648 \u0627\u0644\u062d\u0630\u0641 \u0627\u0644\u062c\u0645\u0627\u0639\u064a \u0644\u0644\u0645\u062d\u0627\u062f\u062b\u0627\u062a" }, "regen_mappings": { - "name": "إعادة إنشاء التعيينات", - "description": "إعادة إنشاء التعيينات يدوياً" + "name": "\u0625\u0639\u0627\u062f\u0629 \u0625\u0646\u0634\u0627\u0621 \u0627\u0644\u062a\u0639\u064a\u064a\u0646\u0627\u062a", + "description": "\u0625\u0639\u0627\u062f\u0629 \u0625\u0646\u0634\u0627\u0621 \u0627\u0644\u062a\u0639\u064a\u064a\u0646\u0627\u062a \u064a\u062f\u0648\u064a\u0627\u064b" }, "change_language": { - "name": "تغيير اللغة", - "description": "تغيير لغة PurrfectSnap" + "name": "\u062a\u063a\u064a\u064a\u0631 \u0627\u0644\u0644\u063a\u0629", + "description": "\u062a\u063a\u064a\u064a\u0631 \u0644\u063a\u0629 PurrfectSnap" }, "file_imports": { - "name": "استيراد الملفات", - "description": "استيراد الملفات للاستخدام في Snapchat" + "name": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0645\u0644\u0641\u0627\u062a", + "description": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0645\u0644\u0641\u0627\u062a \u0644\u0644\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0641\u064a Snapchat" }, "friend_tracker": { - "name": "متتبع الأصدقاء", - "description": "تتبع أصدقائك على Snapchat" + "name": "\u0645\u062a\u062a\u0628\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "description": "\u062a\u062a\u0628\u0639 \u0623\u0635\u062f\u0642\u0627\u0626\u0643 \u0639\u0644\u0649 Snapchat" }, "logger_history": { - "name": "سجل المسجل", - "description": "عرض تاريخ الرسائل المسجلة" + "name": "\u0633\u062c\u0644 \u0627\u0644\u0645\u0633\u062c\u0644", + "description": "\u0639\u0631\u0636 \u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0633\u062c\u0644\u0629" } }, "features": { "notices": { - "unstable": "\u26a0 غير مستقر", - "ban_risk": "\u26a0 قد تسبب هذه الميزة حظراً", - "internal_behavior": "\u26a0 قد يكسر هذا السلوك الداخلي لـ Snapchat" + "unstable": "\u26a0 \u063a\u064a\u0631 \u0645\u0633\u062a\u0642\u0631", + "ban_risk": "\u26a0 \u0642\u062f \u062a\u0633\u0628\u0628 \u0647\u0630\u0647 \u0627\u0644\u0645\u064a\u0632\u0629 \u062d\u0638\u0631\u0627\u064b", + "internal_behavior": "\u26a0 \u0642\u062f \u064a\u0643\u0633\u0631 \u0647\u0630\u0627 \u0627\u0644\u0633\u0644\u0648\u0643 \u0627\u0644\u062f\u0627\u062e\u0644\u064a \u0644\u0640 Snapchat" }, "properties": { "downloader": { - "name": "أداة التنزيل", - "description": "تنزيل وسائط Snapchat", + "name": "\u0623\u062f\u0627\u0629 \u0627\u0644\u062a\u0646\u0632\u064a\u0644", + "description": "\u062a\u0646\u0632\u064a\u0644 \u0648\u0633\u0627\u0626\u0637 Snapchat", "properties": { "save_folder": { - "name": "مجلد الحفظ", - "description": "حدد الدليل الذي يجب تنزيل جميع الوسائط إليه" + "name": "\u0645\u062c\u0644\u062f \u0627\u0644\u062d\u0641\u0638", + "description": "\u062d\u062f\u062f \u0627\u0644\u062f\u0644\u064a\u0644 \u0627\u0644\u0630\u064a \u064a\u062c\u0628 \u062a\u0646\u0632\u064a\u0644 \u062c\u0645\u064a\u0639 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0625\u0644\u064a\u0647" }, "auto_download_sources": { - "name": "مصادر التنزيل التلقائي", - "description": "حدد المصادر للتنزيل منها تلقائياً" + "name": "\u0645\u0635\u0627\u062f\u0631 \u0627\u0644\u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u062d\u062f\u062f \u0627\u0644\u0645\u0635\u0627\u062f\u0631 \u0644\u0644\u062a\u0646\u0632\u064a\u0644 \u0645\u0646\u0647\u0627 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b" }, "prevent_self_auto_download": { - "name": "منع التنزيل التلقائي الذاتي", - "description": "يمنع تنزيل الـ Snaps الخاصة بك تلقائياً" + "name": "\u0645\u0646\u0639 \u0627\u0644\u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0627\u0644\u0630\u0627\u062a\u064a", + "description": "\u064a\u0645\u0646\u0639 \u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0640 Snaps \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0643 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b" }, "path_format": { - "name": "تنسيق المسار", - "description": "حدد تنسيق مسار الملف" + "name": "\u062a\u0646\u0633\u064a\u0642 \u0627\u0644\u0645\u0633\u0627\u0631", + "description": "\u062d\u062f\u062f \u062a\u0646\u0633\u064a\u0642 \u0645\u0633\u0627\u0631 \u0627\u0644\u0645\u0644\u0641" }, "allow_duplicate": { - "name": "السماح بالتكرار", - "description": "يسمح بتنزيل نفس الوسائط عدة مرات" + "name": "\u0627\u0644\u0633\u0645\u0627\u062d \u0628\u0627\u0644\u062a\u0643\u0631\u0627\u0631", + "description": "\u064a\u0633\u0645\u062d \u0628\u062a\u0646\u0632\u064a\u0644 \u0646\u0641\u0633 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0639\u062f\u0629 \u0645\u0631\u0627\u062a" }, "file_hash_check": { - "name": "التحقق من هاش الملف", - "description": "التحقق من الوسائط التي تم تنزيلها باستخدام هاشات الملفات" + "name": "\u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0646 \u0647\u0627\u0634 \u0627\u0644\u0645\u0644\u0641", + "description": "\u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u062a\u064a \u062a\u0645 \u062a\u0646\u0632\u064a\u0644\u0647\u0627 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0647\u0627\u0634\u0627\u062a \u0627\u0644\u0645\u0644\u0641\u0627\u062a" }, "merge_overlays": { - "name": "دمج التراكبات", - "description": "يدمج النص ووسائط الـ Snap في ملف واحد" + "name": "\u062f\u0645\u062c \u0627\u0644\u062a\u0631\u0627\u0643\u0628\u0627\u062a", + "description": "\u064a\u062f\u0645\u062c \u0627\u0644\u0646\u0635 \u0648\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u0640 Snap \u0641\u064a \u0645\u0644\u0641 \u0648\u0627\u062d\u062f" }, "force_image_format": { - "name": "فرض تنسيق الصورة", - "description": "يفرض حفظ الصور بتنسيق محدد" + "name": "\u0641\u0631\u0636 \u062a\u0646\u0633\u064a\u0642 \u0627\u0644\u0635\u0648\u0631\u0629", + "description": "\u064a\u0641\u0631\u0636 \u062d\u0641\u0638 \u0627\u0644\u0635\u0648\u0631 \u0628\u062a\u0646\u0633\u064a\u0642 \u0645\u062d\u062f\u062f" }, "force_voice_note_format": { - "name": "فرض تنسيق الملاحظة الصوتية", - "description": "يفرض حفظ الملاحظات الصوتية بتنسيق محدد" + "name": "\u0641\u0631\u0636 \u062a\u0646\u0633\u064a\u0642 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0629 \u0627\u0644\u0635\u0648\u062a\u064a\u0629", + "description": "\u064a\u0641\u0631\u0636 \u062d\u0641\u0638 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629 \u0628\u062a\u0646\u0633\u064a\u0642 \u0645\u062d\u062f\u062f" }, "auto_download_voice_notes": { - "name": "تنزيل الملاحظات الصوتية تلقائياً", - "description": "يقوم بتنزيل الملاحظات الصوتية تلقائياً عند تشغيلها" + "name": "\u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "description": "\u064a\u0642\u0648\u0645 \u0628\u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0639\u0646\u062f \u062a\u0634\u063a\u064a\u0644\u0647\u0627" }, "call_recorder": { - "name": "مسجل المكالمات", - "description": "إدارة إعدادات تسجيل المكالمات", + "name": "\u0645\u0633\u062c\u0644 \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0627\u062a", + "description": "\u0625\u062f\u0627\u0631\u0629 \u0625\u0639\u062f\u0627\u062f\u0627\u062a \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0627\u062a", "properties": { "call_recorder": { - "name": "الوضع", - "description": "حدد ما يجب تسجيله" + "name": "\u0627\u0644\u0648\u0636\u0639", + "description": "\u062d\u062f\u062f \u0645\u0627 \u064a\u062c\u0628 \u062a\u0633\u062c\u064a\u0644\u0647" }, "auto_start_recording": { - "name": "بدء التسجيل تلقائياً", - "description": "بدء التسجيل تلقائياً عند بدء مكالمة" + "name": "\u0628\u062f\u0621 \u0627\u0644\u062a\u0633\u062c\u064a\u0644 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "description": "\u0628\u062f\u0621 \u0627\u0644\u062a\u0633\u062c\u064a\u0644 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0639\u0646\u062f \u0628\u062f\u0621 \u0645\u0643\u0627\u0644\u0645\u0629" }, "call_recorder_ui": { - "name": "واجهة مسجل المكالمات", - "description": "عرض واجهة تراكب التسجيل أثناء المكالمات" + "name": "\u0648\u0627\u062c\u0647\u0629 \u0645\u0633\u062c\u0644 \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0627\u062a", + "description": "\u0639\u0631\u0636 \u0648\u0627\u062c\u0647\u0629 \u062a\u0631\u0627\u0643\u0628 \u0627\u0644\u062a\u0633\u062c\u064a\u0644 \u0623\u062b\u0646\u0627\u0621 \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0627\u062a" }, "call_recorder_ui_design": { - "name": "تصميم الواجهة", - "description": "حدد تصميم تراكب مسجل المكالمات" + "name": "\u062a\u0635\u0645\u064a\u0645 \u0627\u0644\u0648\u0627\u062c\u0647\u0629", + "description": "\u062d\u062f\u062f \u062a\u0635\u0645\u064a\u0645 \u062a\u0631\u0627\u0643\u0628 \u0645\u0633\u062c\u0644 \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0627\u062a" }, - "call_recording_saved_toast": "تم الحفظ" + "call_recording_saved_toast": "\u062a\u0645 \u0627\u0644\u062d\u0641\u0638" } }, "chat_wallpaper_downloader": { - "name": "تنزيل خلفية الدردشة", - "description": "يسمح لك بتنزيل خلفيات الدردشة من صفحة الملف الشخصي" + "name": "\u062a\u0646\u0632\u064a\u0644 \u062e\u0644\u0641\u064a\u0629 \u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u062a\u0646\u0632\u064a\u0644 \u062e\u0644\u0641\u064a\u0627\u062a \u0627\u0644\u062f\u0631\u062f\u0634\u0629 \u0645\u0646 \u0635\u0641\u062d\u0629 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a" }, "download_profile_pictures": { - "name": "تنزيل صور الملف الشخصي", - "description": "يسمح لك بتنزيل صور الملف الشخصي من صفحة الملف الشخصي" + "name": "\u062a\u0646\u0632\u064a\u0644 \u0635\u0648\u0631 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u062a\u0646\u0632\u064a\u0644 \u0635\u0648\u0631 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a \u0645\u0646 \u0635\u0641\u062d\u0629 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a" }, "opera_download_button": { - "name": "زر تنزيل أوبرا", - "description": "يضيف زر تنزيل في الزاوية اليمنى العليا عند عرض Snap.\nالضغط المطول على الأزرار سيجبر التنزيل" + "name": "\u0632\u0631 \u062a\u0646\u0632\u064a\u0644 \u0623\u0648\u0628\u0631\u0627", + "description": "\u064a\u0636\u064a\u0641 \u0632\u0631 \u062a\u0646\u0632\u064a\u0644 \u0641\u064a \u0627\u0644\u0632\u0627\u0648\u064a\u0629 \u0627\u0644\u064a\u0645\u0646\u0649 \u0627\u0644\u0639\u0644\u064a\u0627 \u0639\u0646\u062f \u0639\u0631\u0636 Snap.\n\u0627\u0644\u0636\u063a\u0637 \u0627\u0644\u0645\u0637\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0623\u0632\u0631\u0627\u0631 \u0633\u064a\u062c\u0628\u0631 \u0627\u0644\u062a\u0646\u0632\u064a\u0644" }, "download_context_menu": { - "name": "تنزيل من قائمة السياق", - "description": "يسمح لك بتنزيل/معاينة الرسائل من محادثة أو قصة باستخدام قائمة السياق.\nالضغط المطول على الأزرار سيجبر التنزيل" + "name": "\u062a\u0646\u0632\u064a\u0644 \u0645\u0646 \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0633\u064a\u0627\u0642", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u062a\u0646\u0632\u064a\u0644/\u0645\u0639\u0627\u064a\u0646\u0629 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0645\u0646 \u0645\u062d\u0627\u062f\u062b\u0629 \u0623\u0648 \u0642\u0635\u0629 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0633\u064a\u0627\u0642.\n\u0627\u0644\u0636\u063a\u0637 \u0627\u0644\u0645\u0637\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0623\u0632\u0631\u0627\u0631 \u0633\u064a\u062c\u0628\u0631 \u0627\u0644\u062a\u0646\u0632\u064a\u0644" }, "ffmpeg_options": { - "name": "خيارات FFmpeg", - "description": "تحديد خيارات FFmpeg إضافية", + "name": "\u062e\u064a\u0627\u0631\u0627\u062a FFmpeg", + "description": "\u062a\u062d\u062f\u064a\u062f \u062e\u064a\u0627\u0631\u0627\u062a FFmpeg \u0625\u0636\u0627\u0641\u064a\u0629", "properties": { "threads": { - "name": "المواضيع (Threads)", - "description": "عدد المواضيع (Threads) للاستخدام" + "name": "\u0627\u0644\u0645\u0648\u0627\u0636\u064a\u0639 (Threads)", + "description": "\u0639\u062f\u062f \u0627\u0644\u0645\u0648\u0627\u0636\u064a\u0639 (Threads) \u0644\u0644\u0627\u0633\u062a\u062e\u062f\u0627\u0645" }, "preset": { - "name": "الإعداد المسبق", - "description": "ضبط سرعة التحويل" + "name": "\u0627\u0644\u0625\u0639\u062f\u0627\u062f \u0627\u0644\u0645\u0633\u0628\u0642", + "description": "\u0636\u0628\u0637 \u0633\u0631\u0639\u0629 \u0627\u0644\u062a\u062d\u0648\u064a\u0644" }, "constant_rate_factor": { - "name": "عامل المعدل الثابت", - "description": "ضبط عامل المعدل الثابت لترميز الفيديو\nمن 0 إلى 51 لـ libx264" + "name": "\u0639\u0627\u0645\u0644 \u0627\u0644\u0645\u0639\u062f\u0644 \u0627\u0644\u062b\u0627\u0628\u062a", + "description": "\u0636\u0628\u0637 \u0639\u0627\u0645\u0644 \u0627\u0644\u0645\u0639\u062f\u0644 \u0627\u0644\u062b\u0627\u0628\u062a \u0644\u062a\u0631\u0645\u064a\u0632 \u0627\u0644\u0641\u064a\u062f\u064a\u0648\n\u0645\u0646 0 \u0625\u0644\u0649 51 \u0644\u0640 libx264" }, "video_bitrate": { - "name": "معدل بت الفيديو", - "description": "ضبط معدل بت الفيديو (kbps)" + "name": "\u0645\u0639\u062f\u0644 \u0628\u062a \u0627\u0644\u0641\u064a\u062f\u064a\u0648", + "description": "\u0636\u0628\u0637 \u0645\u0639\u062f\u0644 \u0628\u062a \u0627\u0644\u0641\u064a\u062f\u064a\u0648 (kbps)" }, "audio_bitrate": { - "name": "معدل بت الصوت", - "description": "ضبط معدل بت الصوت (kbps)" + "name": "\u0645\u0639\u062f\u0644 \u0628\u062a \u0627\u0644\u0635\u0648\u062a", + "description": "\u0636\u0628\u0637 \u0645\u0639\u062f\u0644 \u0628\u062a \u0627\u0644\u0635\u0648\u062a (kbps)" }, "custom_video_codec": { - "name": "ترميز فيديو مخصص", - "description": "تعيين ترميز فيديو مخصص (مثل libx264)" + "name": "\u062a\u0631\u0645\u064a\u0632 \u0641\u064a\u062f\u064a\u0648 \u0645\u062e\u0635\u0635", + "description": "\u062a\u0639\u064a\u064a\u0646 \u062a\u0631\u0645\u064a\u0632 \u0641\u064a\u062f\u064a\u0648 \u0645\u062e\u0635\u0635 (\u0645\u062b\u0644 libx264)" }, "custom_audio_codec": { - "name": "ترميز صوت مخصص", - "description": "تعيين ترميز صوت مخصص (مثل AAC)" + "name": "\u062a\u0631\u0645\u064a\u0632 \u0635\u0648\u062a \u0645\u062e\u0635\u0635", + "description": "\u062a\u0639\u064a\u064a\u0646 \u062a\u0631\u0645\u064a\u0632 \u0635\u0648\u062a \u0645\u062e\u0635\u0635 (\u0645\u062b\u0644 AAC)" } } }, "logging": { - "name": "التسجيل", - "description": "يعرض رسائل منبثقة (Toasts) عند تنزيل الوسائط" + "name": "\u0627\u0644\u062a\u0633\u062c\u064a\u0644", + "description": "\u064a\u0639\u0631\u0636 \u0631\u0633\u0627\u0626\u0644 \u0645\u0646\u0628\u062b\u0642\u0629 (Toasts) \u0639\u0646\u062f \u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637" }, "custom_path_format": { - "name": "تنسيق مسار مخصص", - "description": "حدد تنسيق مسار مخصص للوسائط التي تم تنزيلها\n\nالمتغيرات المتاحة:\n - %username%\n - %source%\n - %hash%\n - %date_time%" + "name": "\u062a\u0646\u0633\u064a\u0642 \u0645\u0633\u0627\u0631 \u0645\u062e\u0635\u0635", + "description": "\u062d\u062f\u062f \u062a\u0646\u0633\u064a\u0642 \u0645\u0633\u0627\u0631 \u0645\u062e\u0635\u0635 \u0644\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u062a\u064a \u062a\u0645 \u062a\u0646\u0632\u064a\u0644\u0647\u0627\n\n\u0627\u0644\u0645\u062a\u063a\u064a\u0631\u0627\u062a \u0627\u0644\u0645\u062a\u0627\u062d\u0629:\n - %username%\n - %source%\n - %hash%\n - %date_time%" } } }, "user_interface": { - "name": "واجهة المستخدم", - "description": "تغيير شكل ومظهر Snapchat", + "name": "\u0648\u0627\u062c\u0647\u0629 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "description": "\u062a\u063a\u064a\u064a\u0631 \u0634\u0643\u0644 \u0648\u0645\u0638\u0647\u0631 Snapchat", "properties": { "enable_app_appearance": { - "name": "تمكين إعدادات مظهر التطبيق", - "description": "يمكن إعداد مظهر التطبيق المخفي\nقد لا يكون مطلوباً في إصدارات Snapchat الأحدث" + "name": "\u062a\u0645\u0643\u064a\u0646 \u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0645\u0638\u0647\u0631 \u0627\u0644\u062a\u0637\u0628\u064a\u0642", + "description": "\u064a\u0645\u0643\u0646 \u0625\u0639\u062f\u0627\u062f \u0645\u0638\u0647\u0631 \u0627\u0644\u062a\u0637\u0628\u064a\u0642 \u0627\u0644\u0645\u062e\u0641\u064a\n\u0642\u062f \u0644\u0627 \u064a\u0643\u0648\u0646 \u0645\u0637\u0644\u0648\u0628\u0627\u064b \u0641\u064a \u0625\u0635\u062f\u0627\u0631\u0627\u062a Snapchat \u0627\u0644\u0623\u062d\u062f\u062b" }, "friend_feed_message_preview": { - "name": "معاينة رسالة موجز الأصدقاء", - "description": "يعرض معاينة لآخر الرسائل في موجز الأصدقاء", + "name": "\u0645\u0639\u0627\u064a\u0646\u0629 \u0631\u0633\u0627\u0644\u0629 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "description": "\u064a\u0639\u0631\u0636 \u0645\u0639\u0627\u064a\u0646\u0629 \u0644\u0622\u062e\u0631 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0641\u064a \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", "properties": { "amount": { - "name": "الكمية", - "description": "عدد الرسائل المراد معاينتها" + "name": "\u0627\u0644\u0643\u0645\u064a\u0629", + "description": "\u0639\u062f\u062f \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0631\u0627\u062f \u0645\u0639\u0627\u064a\u0646\u062a\u0647\u0627" } } }, "snap_preview": { - "name": "معاينة Snap", - "description": "يعرض معاينة صغيرة بجوار الـ Snaps غير المرئية في الدردشة" + "name": "\u0645\u0639\u0627\u064a\u0646\u0629 Snap", + "description": "\u064a\u0639\u0631\u0636 \u0645\u0639\u0627\u064a\u0646\u0629 \u0635\u063a\u064a\u0631\u0629 \u0628\u062c\u0648\u0627\u0631 \u0627\u0644\u0640 Snaps \u063a\u064a\u0631 \u0627\u0644\u0645\u0631\u0626\u064a\u0629 \u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629" }, "bootstrap_override": { - "name": "تجاوز التمهيد (Bootstrap Override)", - "description": "يتجاوز إعدادات تمهيد واجهة المستخدم", + "name": "\u062a\u062c\u0627\u0648\u0632 \u0627\u0644\u062a\u0645\u0647\u064a\u062f (Bootstrap Override)", + "description": "\u064a\u062a\u062c\u0627\u0648\u0632 \u0625\u0639\u062f\u0627\u062f\u0627\u062a \u062a\u0645\u0647\u064a\u062f \u0648\u0627\u062c\u0647\u0629 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", "properties": { "app_appearance": { - "name": "مظهر التطبيق", - "description": "يحدد مظهر التطبيق بشكل دائم" + "name": "\u0645\u0638\u0647\u0631 \u0627\u0644\u062a\u0637\u0628\u064a\u0642", + "description": "\u064a\u062d\u062f\u062f \u0645\u0638\u0647\u0631 \u0627\u0644\u062a\u0637\u0628\u064a\u0642 \u0628\u0634\u0643\u0644 \u062f\u0627\u0626\u0645" }, "home_tab": { - "name": "علامة التبويب الرئيسية", - "description": "يتجاوز علامة تبويب البدء عند فتح Snapchat" + "name": "\u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u062a\u0628\u0648\u064a\u0628 \u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629", + "description": "\u064a\u062a\u062c\u0627\u0648\u0632 \u0639\u0644\u0627\u0645\u0629 \u062a\u0628\u0648\u064a\u0628 \u0627\u0644\u0628\u062f\u0621 \u0639\u0646\u062f \u0641\u062a\u062d Snapchat" } } }, "map_friend_nametags": { - "name": "بطاقات أسماء الأصدقاء المحسنة على الخريطة", - "description": "يحسن بطاقات الأسماء للأصدقاء على خريطة Snap" + "name": "\u0628\u0637\u0627\u0642\u0627\u062a \u0623\u0633\u0645\u0627\u0621 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0627\u0644\u0645\u062d\u0633\u0646\u0629 \u0639\u0644\u0649 \u0627\u0644\u062e\u0631\u064a\u0637\u0629", + "description": "\u064a\u062d\u0633\u0646 \u0628\u0637\u0627\u0642\u0627\u062a \u0627\u0644\u0623\u0633\u0645\u0627\u0621 \u0644\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0639\u0644\u0649 \u062e\u0631\u064a\u0637\u0629 Snap" }, "prevent_message_list_auto_scroll": { - "name": "منع التمرير التلقائي لقائمة الرسائل", - "description": "يمنع قائمة الرسائل من التمرير إلى الأسفل عند إرسال/استقبال رسالة" + "name": "\u0645\u0646\u0639 \u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "description": "\u064a\u0645\u0646\u0639 \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0645\u0646 \u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0625\u0644\u0649 \u0627\u0644\u0623\u0633\u0641\u0644 \u0639\u0646\u062f \u0625\u0631\u0633\u0627\u0644/\u0627\u0633\u062a\u0642\u0628\u0627\u0644 \u0631\u0633\u0627\u0644\u0629" }, "streak_expiration_info": { - "name": "عرض معلومات انتهاء الستريك", - "description": "يعرض مؤقت انتهاء الستريك بجوار عداد الستريك" + "name": "\u0639\u0631\u0636 \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0646\u062a\u0647\u0627\u0621 \u0627\u0644\u0633\u062a\u0631\u064a\u0643", + "description": "\u064a\u0639\u0631\u0636 \u0645\u0624\u0642\u062a \u0627\u0646\u062a\u0647\u0627\u0621 \u0627\u0644\u0633\u062a\u0631\u064a\u0643 \u0628\u062c\u0648\u0627\u0631 \u0639\u062f\u0627\u062f \u0627\u0644\u0633\u062a\u0631\u064a\u0643" }, "hide_friend_feed_entry": { - "name": "إخفاء إدخال موجز الأصدقاء", - "description": "يخفي صديقاً معيناً من موجز الأصدقاء\nاستخدم علامة التبويب الاجتماعية لإدارة هذه الميزة" + "name": "\u0625\u062e\u0641\u0627\u0621 \u0625\u062f\u062e\u0627\u0644 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "description": "\u064a\u062e\u0641\u064a \u0635\u062f\u064a\u0642\u0627\u064b \u0645\u0639\u064a\u0646\u0627\u064b \u0645\u0646 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621\n\u0627\u0633\u062a\u062e\u062f\u0645 \u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u062a\u0628\u0648\u064a\u0628 \u0627\u0644\u0627\u062c\u062a\u0645\u0627\u0639\u064a\u0629 \u0644\u0625\u062f\u0627\u0631\u0629 \u0647\u0630\u0647 \u0627\u0644\u0645\u064a\u0632\u0629" }, "hide_streak_restore": { - "name": "إخفاء استعادة الستريك", - "description": "يخفي زر الاستعادة في موجز الأصدقاء" + "name": "\u0625\u062e\u0641\u0627\u0621 \u0627\u0633\u062a\u0639\u0627\u062f\u0629 \u0627\u0644\u0633\u062a\u0631\u064a\u0643", + "description": "\u064a\u062e\u0641\u064a \u0632\u0631 \u0627\u0644\u0627\u0633\u062a\u0639\u0627\u062f\u0629 \u0641\u064a \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621" }, "hide_quick_add_suggestions": { - "name": "إخفاء اقتراحات الإضافة السريعة", - "description": "يزيل اقتراحات إضافة الأصدقاء السريعة" + "name": "\u0625\u062e\u0641\u0627\u0621 \u0627\u0642\u062a\u0631\u0627\u062d\u0627\u062a \u0627\u0644\u0625\u0636\u0627\u0641\u0629 \u0627\u0644\u0633\u0631\u064a\u0639\u0629", + "description": "\u064a\u0632\u064a\u0644 \u0627\u0642\u062a\u0631\u0627\u062d\u0627\u062a \u0625\u0636\u0627\u0641\u0629 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0627\u0644\u0633\u0631\u064a\u0639\u0629" }, "hide_story_suggestions": { - "name": "إخفاء اقتراحات القصة", - "description": "يزيل الاقتراحات من صفحة القصص" + "name": "\u0625\u062e\u0641\u0627\u0621 \u0627\u0642\u062a\u0631\u0627\u062d\u0627\u062a \u0627\u0644\u0642\u0635\u0629", + "description": "\u064a\u0632\u064a\u0644 \u0627\u0644\u0627\u0642\u062a\u0631\u0627\u062d\u0627\u062a \u0645\u0646 \u0635\u0641\u062d\u0629 \u0627\u0644\u0642\u0635\u0635" }, "hide_ui_components": { - "name": "إخفاء مكونات واجهة المستخدم", - "description": "حدد مكونات واجهة المستخدم التي تريد إخفاءها" + "name": "\u0625\u062e\u0641\u0627\u0621 \u0645\u0643\u0648\u0646\u0627\u062a \u0648\u0627\u062c\u0647\u0629 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "description": "\u062d\u062f\u062f \u0645\u0643\u0648\u0646\u0627\u062a \u0648\u0627\u062c\u0647\u0629 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u062a\u064a \u062a\u0631\u064a\u062f \u0625\u062e\u0641\u0627\u0621\u0647\u0627" }, "opera_media_quick_info": { - "name": "معلومات الوسائط السريعة في أوبرا", - "description": "يعرض معلومات مفيدة عن الوسائط مثل تاريخ الإنشاء في قائمة سياق عارض أوبرا" + "name": "\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u0633\u0631\u064a\u0639\u0629 \u0641\u064a \u0623\u0648\u0628\u0631\u0627", + "description": "\u064a\u0639\u0631\u0636 \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0645\u0641\u064a\u062f\u0629 \u0639\u0646 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0645\u062b\u0644 \u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0625\u0646\u0634\u0627\u0621 \u0641\u064a \u0642\u0627\u0626\u0645\u0629 \u0633\u064a\u0627\u0642 \u0639\u0627\u0631\u0636 \u0623\u0648\u0628\u0631\u0627" }, "story_counter": { - "name": "عداد القصص", - "description": "يعرض عداداً (مثل 1/10) عند مشاهدة القصص" + "name": "\u0639\u062f\u0627\u062f \u0627\u0644\u0642\u0635\u0635", + "description": "\u064a\u0639\u0631\u0636 \u0639\u062f\u0627\u062f\u0627\u064b (\u0645\u062b\u0644 1/10) \u0639\u0646\u062f \u0645\u0634\u0627\u0647\u062f\u0629 \u0627\u0644\u0642\u0635\u0635" }, "story_source_indicator": { - "name": "مؤشر مصدر القصة", - "description": "يعرض أيقونة تشير إلى ما إذا كان السناب تم التقاطه من الكاميرا أو رفعه من المعرض\nيعمل فقط مع قصص الأصدقاء" + "name": "\u0645\u0624\u0634\u0631 \u0645\u0635\u062f\u0631 \u0627\u0644\u0642\u0635\u0629", + "description": "\u064a\u0639\u0631\u0636 \u0623\u064a\u0642\u0648\u0646\u0629 \u062a\u0634\u064a\u0631 \u0625\u0644\u0649 \u0645\u0627 \u0625\u0630\u0627 \u0643\u0627\u0646 \u0627\u0644\u0633\u0646\u0627\u0628 \u062a\u0645 \u0627\u0644\u062a\u0642\u0627\u0637\u0647 \u0645\u0646 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0623\u0648 \u0631\u0641\u0639\u0647 \u0645\u0646 \u0627\u0644\u0645\u0639\u0631\u0636\n\u064a\u0639\u0645\u0644 \u0641\u0642\u0637 \u0645\u0639 \u0642\u0635\u0635 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621" }, "story_snap_jump": { - "name": "تخطي تلقائي", - "description": "يضيف زر تخطي للانتقال إلى أي سناب في القصة. اضغط على أيقونة التخطي أو العداد لفتح نافذة القفز" + "name": "\u062a\u062e\u0637\u064a \u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u064a\u0636\u064a\u0641 \u0632\u0631 \u062a\u062e\u0637\u064a \u0644\u0644\u0627\u0646\u062a\u0642\u0627\u0644 \u0625\u0644\u0649 \u0623\u064a \u0633\u0646\u0627\u0628 \u0641\u064a \u0627\u0644\u0642\u0635\u0629. \u0627\u0636\u063a\u0637 \u0639\u0644\u0649 \u0623\u064a\u0642\u0648\u0646\u0629 \u0627\u0644\u062a\u062e\u0637\u064a \u0623\u0648 \u0627\u0644\u0639\u062f\u0627\u062f \u0644\u0641\u062a\u062d \u0646\u0627\u0641\u0630\u0629 \u0627\u0644\u0642\u0641\u0632" }, "old_bitmoji_selfie": { - "name": "سيلفي Bitmoji القديم", - "description": "يعيد سيلفي Bitmoji من إصدارات Snapchat القديمة" + "name": "\u0633\u064a\u0644\u0641\u064a Bitmoji \u0627\u0644\u0642\u062f\u064a\u0645", + "description": "\u064a\u0639\u064a\u062f \u0633\u064a\u0644\u0641\u064a Bitmoji \u0645\u0646 \u0625\u0635\u062f\u0627\u0631\u0627\u062a Snapchat \u0627\u0644\u0642\u062f\u064a\u0645\u0629" }, "disable_spotlight": { - "name": "تعطيل Spotlight", - "description": "يعطل صفحة Spotlight" + "name": "\u062a\u0639\u0637\u064a\u0644 Spotlight", + "description": "\u064a\u0639\u0637\u0644 \u0635\u0641\u062d\u0629 Spotlight" }, "friend_feed_menu_buttons": { - "name": "أزرار قائمة موجز الأصدقاء", - "description": "حدد الأزرار التي ستظهر في قائمة موجز الأصدقاء" + "name": "\u0623\u0632\u0631\u0627\u0631 \u0642\u0627\u0626\u0645\u0629 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "description": "\u062d\u062f\u062f \u0627\u0644\u0623\u0632\u0631\u0627\u0631 \u0627\u0644\u062a\u064a \u0633\u062a\u0638\u0647\u0631 \u0641\u064a \u0642\u0627\u0626\u0645\u0629 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621" }, "auto_close_friend_feed_menu": { - "name": "إغلاق قائمة موجز الأصدقاء تلقائياً", - "description": "يغلق قائمة موجز الأصدقاء تلقائياً بعد الضغط على زر إعداد" + "name": "\u0625\u063a\u0644\u0627\u0642 \u0642\u0627\u0626\u0645\u0629 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "description": "\u064a\u063a\u0644\u0642 \u0642\u0627\u0626\u0645\u0629 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0628\u0639\u062f \u0627\u0644\u0636\u063a\u0637 \u0639\u0644\u0649 \u0632\u0631 \u0625\u0639\u062f\u0627\u062f" }, "vertical_story_viewer": { - "name": "عارض القصص العمودي", - "description": "يمكن عارض القصص العمودي لجميع القصص" + "name": "\u0639\u0627\u0631\u0636 \u0627\u0644\u0642\u0635\u0635 \u0627\u0644\u0639\u0645\u0648\u062f\u064a", + "description": "\u064a\u0645\u0643\u0646 \u0639\u0627\u0631\u0636 \u0627\u0644\u0642\u0635\u0635 \u0627\u0644\u0639\u0645\u0648\u062f\u064a \u0644\u062c\u0645\u064a\u0639 \u0627\u0644\u0642\u0635\u0635" }, "enable_friend_feed_menu_bar": { - "name": "شريط قائمة موجز الأصدقاء", - "description": "يمكن شريط قائمة موجز الأصدقاء الجديد" + "name": "\u0634\u0631\u064a\u0637 \u0642\u0627\u0626\u0645\u0629 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "description": "\u064a\u0645\u0643\u0646 \u0634\u0631\u064a\u0637 \u0642\u0627\u0626\u0645\u0629 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0627\u0644\u062c\u062f\u064a\u062f" }, "message_indicators": { - "name": "مؤشرات الرسائل", - "description": "يضيف أيقونات مؤشرات محددة للرسائل\nملاحظة: قد لا تكون المؤشرات دقيقة بنسبة 100%" + "name": "\u0645\u0624\u0634\u0631\u0627\u062a \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "description": "\u064a\u0636\u064a\u0641 \u0623\u064a\u0642\u0648\u0646\u0627\u062a \u0645\u0624\u0634\u0631\u0627\u062a \u0645\u062d\u062f\u062f\u0629 \u0644\u0644\u0631\u0633\u0627\u0626\u0644\n\u0645\u0644\u0627\u062d\u0638\u0629: \u0642\u062f \u0644\u0627 \u062a\u0643\u0648\u0646 \u0627\u0644\u0645\u0624\u0634\u0631\u0627\u062a \u062f\u0642\u064a\u0642\u0629 \u0628\u0646\u0633\u0628\u0629 100%" }, "stealth_mode_indicator": { - "name": "مؤشر وضع التخفي", - "description": "يضيف إيموجي \ud83d\udc7b بجوار المحادثات في وضع التخفي" + "name": "\u0645\u0624\u0634\u0631 \u0648\u0636\u0639 \u0627\u0644\u062a\u062e\u0641\u064a", + "description": "\u064a\u0636\u064a\u0641 \u0625\u064a\u0645\u0648\u062c\u064a \ud83d\udc7b \u0628\u062c\u0648\u0627\u0631 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0627\u062a \u0641\u064a \u0648\u0636\u0639 \u0627\u0644\u062a\u062e\u0641\u064a" }, "edit_text_override": { - "name": "تجاوز تحرير النص", - "description": "يتجاوز سلوك حقل النص" + "name": "\u062a\u062c\u0627\u0648\u0632 \u062a\u062d\u0631\u064a\u0631 \u0627\u0644\u0646\u0635", + "description": "\u064a\u062a\u062c\u0627\u0648\u0632 \u0633\u0644\u0648\u0643 \u062d\u0642\u0644 \u0627\u0644\u0646\u0635" }, "prevent_forced_keyboard": { - "name": "منع لوحة المفاتيح الإجبارية", - "description": "يمنع Snapchat من إظهار لوحة المفاتيح تلقائياً عند فتح محادثة" + "name": "\u0645\u0646\u0639 \u0644\u0648\u062d\u0629 \u0627\u0644\u0645\u0641\u0627\u062a\u064a\u062d \u0627\u0644\u0625\u062c\u0628\u0627\u0631\u064a\u0629", + "description": "\u064a\u0645\u0646\u0639 Snapchat \u0645\u0646 \u0625\u0638\u0647\u0627\u0631 \u0644\u0648\u062d\u0629 \u0627\u0644\u0645\u0641\u0627\u062a\u064a\u062d \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0639\u0646\u062f \u0641\u062a\u062d \u0645\u062d\u0627\u062f\u062b\u0629" }, "force_amoled_theme": { - "name": "فرض سمة AMOLED", - "description": "يفرض سمة AMOLED سوداء حقيقية عبر واجهة التطبيق" + "name": "\u0641\u0631\u0636 \u0633\u0645\u0629 AMOLED", + "description": "\u064a\u0641\u0631\u0636 \u0633\u0645\u0629 AMOLED \u0633\u0648\u062f\u0627\u0621 \u062d\u0642\u064a\u0642\u064a\u0629 \u0639\u0628\u0631 \u0648\u0627\u062c\u0647\u0629 \u0627\u0644\u062a\u0637\u0628\u064a\u0642" }, "settings_menu": { - "name": "قائمة الإعدادات", - "description": "اختر بين تخطيطات قائمة الإعدادات الجديدة والقديمة" + "name": "\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0625\u0639\u062f\u0627\u062f\u0627\u062a", + "description": "\u0627\u062e\u062a\u0631 \u0628\u064a\u0646 \u062a\u062e\u0637\u064a\u0637\u0627\u062a \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u062c\u062f\u064a\u062f\u0629 \u0648\u0627\u0644\u0642\u062f\u064a\u0645\u0629" }, "spoof_snap_score": { - "name": "تزييف نقاط سناب شات", - "description": "يقوم بتزييف عدد نقاط سناب شات (المحلية فقط)", + "name": "\u062a\u0632\u064a\u064a\u0641 \u0646\u0642\u0627\u0637 \u0633\u0646\u0627\u0628 \u0634\u0627\u062a", + "description": "\u064a\u0642\u0648\u0645 \u0628\u062a\u0632\u064a\u064a\u0641 \u0639\u062f\u062f \u0646\u0642\u0627\u0637 \u0633\u0646\u0627\u0628 \u0634\u0627\u062a (\u0627\u0644\u0645\u062d\u0644\u064a\u0629 \u0641\u0642\u0637)", "properties": { "custom_snap_score": { - "name": "النقاط المخصصة", - "description": "تعيين نقاط السناب شات الوهمية (أقصى عدد هو 9,999,999)" + "name": "\u0627\u0644\u0646\u0642\u0627\u0637 \u0627\u0644\u0645\u062e\u0635\u0635\u0629", + "description": "\u062a\u0639\u064a\u064a\u0646 \u0646\u0642\u0627\u0637 \u0627\u0644\u0633\u0646\u0627\u0628 \u0634\u0627\u062a \u0627\u0644\u0648\u0647\u0645\u064a\u0629 (\u0623\u0642\u0635\u0649 \u0639\u062f\u062f \u0647\u0648 9,999,999)" + } + } + }, + "spoof_followers_count": { + "name": "\u062a\u0632\u064a\u064a\u0641 \u0639\u062f\u062f \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u064a\u0646", + "description": "\u064a\u0639\u0631\u0636 \u0639\u062f\u062f \u0645\u062a\u0627\u0628\u0639\u064a\u0646 \u0645\u0632\u064a\u0651\u0641\u0627\u064b \u0639\u0644\u0649 \u0645\u0644\u0641\u0643 (\u0645\u062d\u0644\u064a \u0641\u0642\u0637).", + "properties": { + "custom_followers_count": { + "name": "\u0639\u062f\u062f \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u064a\u0646 \u0627\u0644\u0645\u062e\u0635\u0635", + "description": "\u0627\u0644\u0631\u0642\u0645 \u0627\u0644\u0645\u0639\u0631\u0648\u0636 (\u0623\u0631\u0642\u0627\u0645 \u0641\u0642\u0637)." } } } } }, "messaging": { - "name": "المراسلة", - "description": "تغيير طريقة تفاعلك مع الأصدقاء", + "name": "\u0627\u0644\u0645\u0631\u0627\u0633\u0644\u0629", + "description": "\u062a\u063a\u064a\u064a\u0631 \u0637\u0631\u064a\u0642\u0629 \u062a\u0641\u0627\u0639\u0644\u0643 \u0645\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", "properties": { "bypass_screenshot_detection": { - "name": "تجاوز كشف لقطة الشاشة", - "description": "يمنع Snapchat من اكتشاف قيامك بأخذ لقطة شاشة" + "name": "\u062a\u062c\u0627\u0648\u0632 \u0643\u0634\u0641 \u0644\u0642\u0637\u0629 \u0627\u0644\u0634\u0627\u0634\u0629", + "description": "\u064a\u0645\u0646\u0639 Snapchat \u0645\u0646 \u0627\u0643\u062a\u0634\u0627\u0641 \u0642\u064a\u0627\u0645\u0643 \u0628\u0623\u062e\u0630 \u0644\u0642\u0637\u0629 \u0634\u0627\u0634\u0629" }, "anonymous_story_viewing": { - "name": "عرض القصة المجهول", - "description": "يمنع أي شخص من معرفة أنك شاهدت قصته" + "name": "\u0639\u0631\u0636 \u0627\u0644\u0642\u0635\u0629 \u0627\u0644\u0645\u062c\u0647\u0648\u0644", + "description": "\u064a\u0645\u0646\u0639 \u0623\u064a \u0634\u062e\u0635 \u0645\u0646 \u0645\u0639\u0631\u0641\u0629 \u0623\u0646\u0643 \u0634\u0627\u0647\u062f\u062a \u0642\u0635\u062a\u0647" }, "prevent_story_rewatch_indicator": { - "name": "منع مؤشر إعادة مشاهدة القصة", - "description": "يمنع أي شخص من معرفة أنك أعدت مشاهدة قصته" + "name": "\u0645\u0646\u0639 \u0645\u0624\u0634\u0631 \u0625\u0639\u0627\u062f\u0629 \u0645\u0634\u0627\u0647\u062f\u0629 \u0627\u0644\u0642\u0635\u0629", + "description": "\u064a\u0645\u0646\u0639 \u0623\u064a \u0634\u062e\u0635 \u0645\u0646 \u0645\u0639\u0631\u0641\u0629 \u0623\u0646\u0643 \u0623\u0639\u062f\u062a \u0645\u0634\u0627\u0647\u062f\u0629 \u0642\u0635\u062a\u0647" }, "hide_peek_a_peek": { - "name": "إخفاء Peek-a-Peek", - "description": "يمنع إرسال إشعار عند التمرير النصفي (half swipe) داخل الدردشة" + "name": "\u0625\u062e\u0641\u0627\u0621 Peek-a-Peek", + "description": "\u064a\u0645\u0646\u0639 \u0625\u0631\u0633\u0627\u0644 \u0625\u0634\u0639\u0627\u0631 \u0639\u0646\u062f \u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a (half swipe) \u062f\u0627\u062e\u0644 \u0627\u0644\u062f\u0631\u062f\u0634\u0629" }, "hide_bitmoji_presence": { - "name": "إخفاء وجود Bitmoji", - "description": "يمنع ظهور Bitmoji الخاص بك أثناء وجودك في الدردشة" + "name": "\u0625\u062e\u0641\u0627\u0621 \u0648\u062c\u0648\u062f Bitmoji", + "description": "\u064a\u0645\u0646\u0639 \u0638\u0647\u0648\u0631 Bitmoji \u0627\u0644\u062e\u0627\u0635 \u0628\u0643 \u0623\u062b\u0646\u0627\u0621 \u0648\u062c\u0648\u062f\u0643 \u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629" }, "hide_typing_notifications": { - "name": "إخفاء إشعارات الكتابة", - "description": "يمنع أي شخص من معرفة أنك تكتب رسالة" + "name": "\u0625\u062e\u0641\u0627\u0621 \u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0627\u0644\u0643\u062a\u0627\u0628\u0629", + "description": "\u064a\u0645\u0646\u0639 \u0623\u064a \u0634\u062e\u0635 \u0645\u0646 \u0645\u0639\u0631\u0641\u0629 \u0623\u0646\u0643 \u062a\u0643\u062a\u0628 \u0631\u0633\u0627\u0644\u0629" }, "unlimited_snap_view_time": { - "name": "وقت عرض Snap غير محدود", - "description": "يزيل الحد الزمني لعرض الـ Snaps" + "name": "\u0648\u0642\u062a \u0639\u0631\u0636 Snap \u063a\u064a\u0631 \u0645\u062d\u062f\u0648\u062f", + "description": "\u064a\u0632\u064a\u0644 \u0627\u0644\u062d\u062f \u0627\u0644\u0632\u0645\u0646\u064a \u0644\u0639\u0631\u0636 \u0627\u0644\u0640 Snaps" }, "auto_mark_as_read": { - "name": "وضع علامة مقروء تلقائياً", - "description": "يضع علامة مقروء على الرسائل/snaps تلقائياً حتى عند تمكين وضع التخفي" + "name": "\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0645\u0642\u0631\u0648\u0621 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "description": "\u064a\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0645\u0642\u0631\u0648\u0621 \u0639\u0644\u0649 \u0627\u0644\u0631\u0633\u0627\u0626\u0644/snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u062d\u062a\u0649 \u0639\u0646\u062f \u062a\u0645\u0643\u064a\u0646 \u0648\u0636\u0639 \u0627\u0644\u062a\u062e\u0641\u064a" }, "mark_snap_as_seen_button": { - "name": "زر وضع علامة \"تمت المشاهدة\" على Snap", - "description": "يضيف زراً لوضع علامة \"تمت المشاهدة\" على Snap عند عرضه.\nسيعمل هذا حتى عند تمكين وضع التخفي" + "name": "\u0632\u0631 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\" \u0639\u0644\u0649 Snap", + "description": "\u064a\u0636\u064a\u0641 \u0632\u0631\u0627\u064b \u0644\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\" \u0639\u0644\u0649 Snap \u0639\u0646\u062f \u0639\u0631\u0636\u0647.\n\u0633\u064a\u0639\u0645\u0644 \u0647\u0630\u0627 \u062d\u062a\u0649 \u0639\u0646\u062f \u062a\u0645\u0643\u064a\u0646 \u0648\u0636\u0639 \u0627\u0644\u062a\u062e\u0641\u064a" }, "skip_when_marking_as_seen": { - "name": "تخطي عند وضع علامة \"تمت المشاهدة\"", - "description": "يتخطى تلقائياً إلى الـ Snap التالي عند وضع علامة \"تمت المشاهدة\" على Snap.\nاستخدمه بالاشتراك مع زر وضع علامة \"تمت المشاهدة\" على Snap" + "name": "\u062a\u062e\u0637\u064a \u0639\u0646\u062f \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\"", + "description": "\u064a\u062a\u062e\u0637\u0649 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0625\u0644\u0649 \u0627\u0644\u0640 Snap \u0627\u0644\u062a\u0627\u0644\u064a \u0639\u0646\u062f \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\" \u0639\u0644\u0649 Snap.\n\u0627\u0633\u062a\u062e\u062f\u0645\u0647 \u0628\u0627\u0644\u0627\u0634\u062a\u0631\u0627\u0643 \u0645\u0639 \u0632\u0631 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\" \u0639\u0644\u0649 Snap" }, "loop_media_playback": { - "name": "تكرار تشغيل الوسائط", - "description": "يكرر تشغيل الوسائط عند عرض الـ Snaps / القصص" + "name": "\u062a\u0643\u0631\u0627\u0631 \u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637", + "description": "\u064a\u0643\u0631\u0631 \u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0639\u0646\u062f \u0639\u0631\u0636 \u0627\u0644\u0640 Snaps / \u0627\u0644\u0642\u0635\u0635" }, "disable_replay_in_ff": { - "name": "تعطيل إعادة التشغيل في موجز الأصدقاء", - "description": "يعطل القدرة على إعادة التشغيل بضغطة طويلة من موجز الأصدقاء" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0625\u0639\u0627\u062f\u0629 \u0627\u0644\u062a\u0634\u063a\u064a\u0644 \u0641\u064a \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "description": "\u064a\u0639\u0637\u0644 \u0627\u0644\u0642\u062f\u0631\u0629 \u0639\u0644\u0649 \u0625\u0639\u0627\u062f\u0629 \u0627\u0644\u062a\u0634\u063a\u064a\u0644 \u0628\u0636\u063a\u0637\u0629 \u0637\u0648\u064a\u0644\u0629 \u0645\u0646 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621" }, "half_swipe_notifier": { - "name": "منبه التمرير النصفي", - "description": "ينبهك عندما يقوم شخص ما بالتمرير النصفي في محادثة", + "name": "\u0645\u0646\u0628\u0647 \u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a", + "description": "\u064a\u0646\u0628\u0647\u0643 \u0639\u0646\u062f\u0645\u0627 \u064a\u0642\u0648\u0645 \u0634\u062e\u0635 \u0645\u0627 \u0628\u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a \u0641\u064a \u0645\u062d\u0627\u062f\u062b\u0629", "properties": { "min_duration": { - "name": "الحد الأدنى للمدة", - "description": "الحد الأدنى لمدة التمرير النصفي (بالثواني)" + "name": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u062f\u0646\u0649 \u0644\u0644\u0645\u062f\u0629", + "description": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u062f\u0646\u0649 \u0644\u0645\u062f\u0629 \u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a (\u0628\u0627\u0644\u062b\u0648\u0627\u0646\u064a)" }, "max_duration": { - "name": "الحد الأقصى للمدة", - "description": "الحد الأقصى لمدة التمرير النصفي (بالثواني)" + "name": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0644\u0645\u062f\u0629", + "description": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0645\u062f\u0629 \u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a (\u0628\u0627\u0644\u062b\u0648\u0627\u0646\u064a)" } } }, "call_start_confirmation": { - "name": "تأكيد بدء المكالمة", - "description": "يعرض مربع حوار تأكيد عند بدء مكالمة" + "name": "\u062a\u0623\u0643\u064a\u062f \u0628\u062f\u0621 \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0629", + "description": "\u064a\u0639\u0631\u0636 \u0645\u0631\u0628\u0639 \u062d\u0648\u0627\u0631 \u062a\u0623\u0643\u064a\u062f \u0639\u0646\u062f \u0628\u062f\u0621 \u0645\u0643\u0627\u0644\u0645\u0629" }, "unlimited_conversation_pinning": { - "name": "تثبيت محادثات غير محدود", - "description": "يسمح لك بتثبيت عدد غير محدود من المحادثات محلياً" + "name": "\u062a\u062b\u0628\u064a\u062a \u0645\u062d\u0627\u062f\u062b\u0627\u062a \u063a\u064a\u0631 \u0645\u062d\u062f\u0648\u062f", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u062a\u062b\u0628\u064a\u062a \u0639\u062f\u062f \u063a\u064a\u0631 \u0645\u062d\u062f\u0648\u062f \u0645\u0646 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0627\u062a \u0645\u062d\u0644\u064a\u0627\u064b" }, "disable_snap_mode_restrictions": { - "name": "تعطيل قيود وضع Snap", - "description": "يسمح لك بعرض الـ Snaps ذاتية التدمير دون قيود" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0642\u064a\u0648\u062f \u0648\u0636\u0639 Snap", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u0639\u0631\u0636 \u0627\u0644\u0640 Snaps \u0630\u0627\u062a\u064a\u0629 \u0627\u0644\u062a\u062f\u0645\u064a\u0631 \u062f\u0648\u0646 \u0642\u064a\u0648\u062f" }, "prevent_message_sending": { - "name": "منع إرسال الرسائل", - "description": "يمنع إرسال أنواع معينة من الرسائل" + "name": "\u0645\u0646\u0639 \u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "description": "\u064a\u0645\u0646\u0639 \u0625\u0631\u0633\u0627\u0644 \u0623\u0646\u0648\u0627\u0639 \u0645\u0639\u064a\u0646\u0629 \u0645\u0646 \u0627\u0644\u0631\u0633\u0627\u0626\u0644" }, "friend_mutation_notifier": { - "name": "منبه تغييرات الصديق", - "description": "ينبهك عندما يتغير شيء ما في ملف تعريف الصديق" + "name": "\u0645\u0646\u0628\u0647 \u062a\u063a\u064a\u064a\u0631\u0627\u062a \u0627\u0644\u0635\u062f\u064a\u0642", + "description": "\u064a\u0646\u0628\u0647\u0643 \u0639\u0646\u062f\u0645\u0627 \u064a\u062a\u063a\u064a\u0631 \u0634\u064a\u0621 \u0645\u0627 \u0641\u064a \u0645\u0644\u0641 \u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0635\u062f\u064a\u0642" }, "better_notifications": { - "name": "إشعارات أفضل", - "description": "يضيف المزيد من المعلومات في الإشعارات الواردة", + "name": "\u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0623\u0641\u0636\u0644", + "description": "\u064a\u0636\u064a\u0641 \u0627\u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0641\u064a \u0627\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0627\u0644\u0648\u0627\u0631\u062f\u0629", "properties": { "group_notifications": { - "name": "إشعارات المجموعة", - "description": "تجميع الإشعارات في واحد" + "name": "\u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0629", + "description": "\u062a\u062c\u0645\u064a\u0639 \u0627\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0641\u064a \u0648\u0627\u062d\u062f" }, "chat_preview": { - "name": "معاينة الدردشة", - "description": "يعرض معاينة للرسائل المستلمة في الإشعار" + "name": "\u0645\u0639\u0627\u064a\u0646\u0629 \u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "description": "\u064a\u0639\u0631\u0636 \u0645\u0639\u0627\u064a\u0646\u0629 \u0644\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0633\u062a\u0644\u0645\u0629 \u0641\u064a \u0627\u0644\u0625\u0634\u0639\u0627\u0631" }, "media_preview": { - "name": "معاينة الوسائط", - "description": "يعرض معاينة لأنواع الوسائط المحددة في الإشعار" + "name": "\u0645\u0639\u0627\u064a\u0646\u0629 \u0627\u0644\u0648\u0633\u0627\u0626\u0637", + "description": "\u064a\u0639\u0631\u0636 \u0645\u0639\u0627\u064a\u0646\u0629 \u0644\u0623\u0646\u0648\u0627\u0639 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u0645\u062d\u062f\u062f\u0629 \u0641\u064a \u0627\u0644\u0625\u0634\u0639\u0627\u0631" }, "media_caption": { - "name": "تسمية الوسائط", - "description": "يعرض التسمية التوضيحية المرفقة للوسائط في الإشعار" + "name": "\u062a\u0633\u0645\u064a\u0629 \u0627\u0644\u0648\u0633\u0627\u0626\u0637", + "description": "\u064a\u0639\u0631\u0636 \u0627\u0644\u062a\u0633\u0645\u064a\u0629 \u0627\u0644\u062a\u0648\u0636\u064a\u062d\u064a\u0629 \u0627\u0644\u0645\u0631\u0641\u0642\u0629 \u0644\u0644\u0648\u0633\u0627\u0626\u0637 \u0641\u064a \u0627\u0644\u0625\u0634\u0639\u0627\u0631" }, "stacked_media_messages": { - "name": "رسائل الوسائط المكدسة", - "description": "يجمع رسائل وسائط متعددة في إشعار نصي واحد عندما لا يمكن معاينتها. استخدمه بالاشتراك مع معاينة الدردشة" + "name": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u0645\u0643\u062f\u0633\u0629", + "description": "\u064a\u062c\u0645\u0639 \u0631\u0633\u0627\u0626\u0644 \u0648\u0633\u0627\u0626\u0637 \u0645\u062a\u0639\u062f\u062f\u0629 \u0641\u064a \u0625\u0634\u0639\u0627\u0631 \u0646\u0635\u064a \u0648\u0627\u062d\u062f \u0639\u0646\u062f\u0645\u0627 \u0644\u0627 \u064a\u0645\u0643\u0646 \u0645\u0639\u0627\u064a\u0646\u062a\u0647\u0627. \u0627\u0633\u062a\u062e\u062f\u0645\u0647 \u0628\u0627\u0644\u0627\u0634\u062a\u0631\u0627\u0643 \u0645\u0639 \u0645\u0639\u0627\u064a\u0646\u0629 \u0627\u0644\u062f\u0631\u062f\u0634\u0629" }, "friend_add_source": { - "name": "مصدر إضافة صديق", - "description": "يعرض مصدر طلب الصداقة في الإشعار" + "name": "\u0645\u0635\u062f\u0631 \u0625\u0636\u0627\u0641\u0629 \u0635\u062f\u064a\u0642", + "description": "\u064a\u0639\u0631\u0636 \u0645\u0635\u062f\u0631 \u0637\u0644\u0628 \u0627\u0644\u0635\u062f\u0627\u0642\u0629 \u0641\u064a \u0627\u0644\u0625\u0634\u0639\u0627\u0631" }, "reply_button": { - "name": "زر الرد", - "description": "يضيف زر رد إلى الإشعار" + "name": "\u0632\u0631 \u0627\u0644\u0631\u062f", + "description": "\u064a\u0636\u064a\u0641 \u0632\u0631 \u0631\u062f \u0625\u0644\u0649 \u0627\u0644\u0625\u0634\u0639\u0627\u0631" }, "smart_replies": { - "name": "الردود الذكية", - "description": "يضيف ردوداً مقترحة إلى الإشعارات (Android 10+). استخدمه بالاشتراك مع زر الرد" + "name": "\u0627\u0644\u0631\u062f\u0648\u062f \u0627\u0644\u0630\u0643\u064a\u0629", + "description": "\u064a\u0636\u064a\u0641 \u0631\u062f\u0648\u062f\u0627\u064b \u0645\u0642\u062a\u0631\u062d\u0629 \u0625\u0644\u0649 \u0627\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062a (Android 10+). \u0627\u0633\u062a\u062e\u062f\u0645\u0647 \u0628\u0627\u0644\u0627\u0634\u062a\u0631\u0627\u0643 \u0645\u0639 \u0632\u0631 \u0627\u0644\u0631\u062f" }, "download_button": { - "name": "زر التنزيل", - "description": "يسمح لك بتنزيل الوسائط من الإشعار" + "name": "\u0632\u0631 \u0627\u0644\u062a\u0646\u0632\u064a\u0644", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0645\u0646 \u0627\u0644\u0625\u0634\u0639\u0627\u0631" }, "mark_as_read_button": { - "name": "زر وضع علامة كمقروء", - "description": "يسمح لك بوضع علامة كمقروء على رسالة من الإشعار" + "name": "\u0632\u0631 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0643\u0645\u0642\u0631\u0648\u0621", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0643\u0645\u0642\u0631\u0648\u0621 \u0639\u0644\u0649 \u0631\u0633\u0627\u0644\u0629 \u0645\u0646 \u0627\u0644\u0625\u0634\u0639\u0627\u0631" }, "mark_as_read_and_save_in_chat": { - "name": "وضع علامة كمقروء وحفظ في الدردشة", - "description": "يضيف زر وضع علامة كمقروء وحفظ في الدردشة إلى الإشعار" + "name": "\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0643\u0645\u0642\u0631\u0648\u0621 \u0648\u062d\u0641\u0638 \u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "description": "\u064a\u0636\u064a\u0641 \u0632\u0631 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0643\u0645\u0642\u0631\u0648\u0621 \u0648\u062d\u0641\u0638 \u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629 \u0625\u0644\u0649 \u0627\u0644\u0625\u0634\u0639\u0627\u0631" } } }, "notification_blacklist": { - "name": "القائمة السوداء للإشعارات", - "description": "حدد الإشعارات التي يجب حظرها" + "name": "\u0627\u0644\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0633\u0648\u062f\u0627\u0621 \u0644\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062a", + "description": "\u062d\u062f\u062f \u0627\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0627\u0644\u062a\u064a \u064a\u062c\u0628 \u062d\u0638\u0631\u0647\u0627" }, "message_logger": { - "name": "مسجل الرسائل", - "description": "يمنع حذف الرسائل", + "name": "\u0645\u0633\u062c\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "description": "\u064a\u0645\u0646\u0639 \u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", "properties": { "keep_my_own_messages": { - "name": "الاحتفاظ برسائلي الخاصة", - "description": "يمنع حذف رسائلك الخاصة" + "name": "\u0627\u0644\u0627\u062d\u062a\u0641\u0627\u0638 \u0628\u0631\u0633\u0627\u0626\u0644\u064a \u0627\u0644\u062e\u0627\u0635\u0629", + "description": "\u064a\u0645\u0646\u0639 \u062d\u0630\u0641 \u0631\u0633\u0627\u0626\u0644\u0643 \u0627\u0644\u062e\u0627\u0635\u0629" }, "auto_purge": { - "name": "تطهير تلقائي", - "description": "يحذف الرسائل المخبأة تلقائياً التي أقدم من المدة المحددة" + "name": "\u062a\u0637\u0647\u064a\u0631 \u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u064a\u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u062e\u0628\u0623\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0627\u0644\u062a\u064a \u0623\u0642\u062f\u0645 \u0645\u0646 \u0627\u0644\u0645\u062f\u0629 \u0627\u0644\u0645\u062d\u062f\u062f\u0629" }, "message_filter": { - "name": "فلتر الرسائل", - "description": "حدد الرسائل التي يجب تسجيلها (فارغ لجميع الرسائل)" + "name": "\u0641\u0644\u062a\u0631 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "description": "\u062d\u062f\u062f \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062a\u064a \u064a\u062c\u0628 \u062a\u0633\u062c\u064a\u0644\u0647\u0627 (\u0641\u0627\u0631\u063a \u0644\u062c\u0645\u064a\u0639 \u0627\u0644\u0631\u0633\u0627\u0626\u0644)" }, "deleted_message_color": { - "name": "لون الرسالة المحذوفة", - "description": "يحدد لون الرسائل المحذوفة" + "name": "\u0644\u0648\u0646 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u0627\u0644\u0645\u062d\u0630\u0648\u0641\u0629", + "description": "\u064a\u062d\u062f\u062f \u0644\u0648\u0646 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u062d\u0630\u0648\u0641\u0629" } } }, "auto_save_messages_in_conversations": { - "name": "حفظ الرسائل تلقائياً", - "description": "يحفظ كل رسالة في المحادثات تلقائياً" + "name": "\u062d\u0641\u0638 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "description": "\u064a\u062d\u0641\u0638 \u0643\u0644 \u0631\u0633\u0627\u0644\u0629 \u0641\u064a \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0627\u062a \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b" }, "unsaveable_messages": { - "name": "رسائل غير قابلة للحفظ", - "description": "يمنع حفظ أنواع الرسائل المحددة في الدردشة", + "name": "\u0631\u0633\u0627\u0626\u0644 \u063a\u064a\u0631 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638", + "description": "\u064a\u0645\u0646\u0639 \u062d\u0641\u0638 \u0623\u0646\u0648\u0627\u0639 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u062d\u062f\u062f\u0629 \u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629", "properties": { "chat": { - "name": "رسائل الدردشة", - "description": "جعل رسائل الدردشة غير قابلة للحفظ" + "name": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "description": "\u062c\u0639\u0644 \u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062f\u0631\u062f\u0634\u0629 \u063a\u064a\u0631 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638" }, "snap": { "name": "Snaps", - "description": "جعل الـ Snaps غير قابلة للحفظ" + "description": "\u062c\u0639\u0644 \u0627\u0644\u0640 Snaps \u063a\u064a\u0631 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638" }, "external_media": { - "name": "الوسائط الخارجية", - "description": "جعل الوسائط الخارجية غير قابلة للحفظ" + "name": "\u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u062e\u0627\u0631\u062c\u064a\u0629", + "description": "\u062c\u0639\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u062e\u0627\u0631\u062c\u064a\u0629 \u063a\u064a\u0631 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638" }, "sticker": { - "name": "الملصقات", - "description": "جعل الملصقات غير قابلة للحفظ" + "name": "\u0627\u0644\u0645\u0644\u0635\u0642\u0627\u062a", + "description": "\u062c\u0639\u0644 \u0627\u0644\u0645\u0644\u0635\u0642\u0627\u062a \u063a\u064a\u0631 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638" }, "share": { - "name": "المشاركات", - "description": "جعل المحتوى المشترك غير قابل للحفظ" + "name": "\u0627\u0644\u0645\u0634\u0627\u0631\u0643\u0627\u062a", + "description": "\u062c\u0639\u0644 \u0627\u0644\u0645\u062d\u062a\u0648\u0649 \u0627\u0644\u0645\u0634\u062a\u0631\u0643 \u063a\u064a\u0631 \u0642\u0627\u0628\u0644 \u0644\u0644\u062d\u0641\u0638" }, "note": { - "name": "الملاحظات الصوتية", - "description": "جعل الملاحظات الصوتية غير قابلة للحفظ" + "name": "\u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629", + "description": "\u062c\u0639\u0644 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629 \u063a\u064a\u0631 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638" }, "story_reply": { - "name": "ردود القصة", - "description": "جعل ردود القصة غير قابلة للحفظ" + "name": "\u0631\u062f\u0648\u062f \u0627\u0644\u0642\u0635\u0629", + "description": "\u062c\u0639\u0644 \u0631\u062f\u0648\u062f \u0627\u0644\u0642\u0635\u0629 \u063a\u064a\u0631 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638" } } }, "gallery_media_send_override": { - "name": "تجاوز إرسال وسائط المعرض", - "description": "يقوم بتزييف مصدر الوسائط عند الإرسال من المعرض", + "name": "\u062a\u062c\u0627\u0648\u0632 \u0625\u0631\u0633\u0627\u0644 \u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u0645\u0639\u0631\u0636", + "description": "\u064a\u0642\u0648\u0645 \u0628\u062a\u0632\u064a\u064a\u0641 \u0645\u0635\u062f\u0631 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0639\u0646\u062f \u0627\u0644\u0625\u0631\u0633\u0627\u0644 \u0645\u0646 \u0627\u0644\u0645\u0639\u0631\u0636", "properties": { "mode": { - "name": "وضع التجاوز", - "description": "اختر كيفية إرسال وسائط المعرض" + "name": "\u0648\u0636\u0639 \u0627\u0644\u062a\u062c\u0627\u0648\u0632", + "description": "\u0627\u062e\u062a\u0631 \u0643\u064a\u0641\u064a\u0629 \u0625\u0631\u0633\u0627\u0644 \u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u0645\u0639\u0631\u0636" }, "include_camera_snaps": { - "name": "تضمين Snaps الكاميرا", - "description": "عرض مربع حوار التجاوز أيضاً لـ Snaps الكاميرا" + "name": "\u062a\u0636\u0645\u064a\u0646 Snaps \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627", + "description": "\u0639\u0631\u0636 \u0645\u0631\u0628\u0639 \u062d\u0648\u0627\u0631 \u0627\u0644\u062a\u062c\u0627\u0648\u0632 \u0623\u064a\u0636\u0627\u064b \u0644\u0640 Snaps \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627" } } }, "strip_media_metadata": { - "name": "تجريد البيانات الوصفية للوسائط", - "description": "يزيل البيانات الوصفية للوسائط قبل إرسالها كرسالة" + "name": "\u062a\u062c\u0631\u064a\u062f \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0648\u0635\u0641\u064a\u0629 \u0644\u0644\u0648\u0633\u0627\u0626\u0637", + "description": "\u064a\u0632\u064a\u0644 \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0648\u0635\u0641\u064a\u0629 \u0644\u0644\u0648\u0633\u0627\u0626\u0637 \u0642\u0628\u0644 \u0625\u0631\u0633\u0627\u0644\u0647\u0627 \u0643\u0631\u0633\u0627\u0644\u0629" }, "bypass_message_retention_policy": { - "name": "تجاوز سياسة الاحتفاظ بالرسائل", - "description": "يمنع حذف الرسائل بعد مشاهدتها" + "name": "\u062a\u062c\u0627\u0648\u0632 \u0633\u064a\u0627\u0633\u0629 \u0627\u0644\u0627\u062d\u062a\u0641\u0627\u0638 \u0628\u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "description": "\u064a\u0645\u0646\u0639 \u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0628\u0639\u062f \u0645\u0634\u0627\u0647\u062f\u062a\u0647\u0627" }, "bypass_message_action_restrictions": { - "name": "تجاوز قيود إجراءات الرسالة", - "description": "يسمح لك بالتفاعل مع snap دون فتحه أو حفظ رسالة غير قابلة للحفظ" + "name": "\u062a\u062c\u0627\u0648\u0632 \u0642\u064a\u0648\u062f \u0625\u062c\u0631\u0627\u0621\u0627\u062a \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u0627\u0644\u062a\u0641\u0627\u0639\u0644 \u0645\u0639 snap \u062f\u0648\u0646 \u0641\u062a\u062d\u0647 \u0623\u0648 \u062d\u0641\u0638 \u0631\u0633\u0627\u0644\u0629 \u063a\u064a\u0631 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638" }, "remove_groups_locked_status": { - "name": "إزالة حالة القفل للمجموعات", - "description": "يسمح لك بعرض معلومات المجموعة بعد طردك منها" + "name": "\u0625\u0632\u0627\u0644\u0629 \u062d\u0627\u0644\u0629 \u0627\u0644\u0642\u0641\u0644 \u0644\u0644\u0645\u062c\u0645\u0648\u0639\u0627\u062a", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u0639\u0631\u0636 \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0629 \u0628\u0639\u062f \u0637\u0631\u062f\u0643 \u0645\u0646\u0647\u0627" }, "double_tap_chat_action": { - "name": "إجراء النقر المزدوج في الدردشة", - "description": "ينفذ إجراءً مخصصاً عند النقر المزدوج على رسالة في الدردشة" + "name": "\u0625\u062c\u0631\u0627\u0621 \u0627\u0644\u0646\u0642\u0631 \u0627\u0644\u0645\u0632\u062f\u0648\u062c \u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "description": "\u064a\u0646\u0641\u0630 \u0625\u062c\u0631\u0627\u0621\u064b \u0645\u062e\u0635\u0635\u0627\u064b \u0639\u0646\u062f \u0627\u0644\u0646\u0642\u0631 \u0627\u0644\u0645\u0632\u062f\u0648\u062c \u0639\u0644\u0649 \u0631\u0633\u0627\u0644\u0629 \u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629" }, "double_tap_chat_action_custom_emoji": { - "name": "تفاعل إيموجي مخصص لإجراء النقر المزدوج", - "description": "يحدد تفاعل إيموجي مخصص لإجراء النقر المزدوج في الدردشة" + "name": "\u062a\u0641\u0627\u0639\u0644 \u0625\u064a\u0645\u0648\u062c\u064a \u0645\u062e\u0635\u0635 \u0644\u0625\u062c\u0631\u0627\u0621 \u0627\u0644\u0646\u0642\u0631 \u0627\u0644\u0645\u0632\u062f\u0648\u062c", + "description": "\u064a\u062d\u062f\u062f \u062a\u0641\u0627\u0639\u0644 \u0625\u064a\u0645\u0648\u062c\u064a \u0645\u062e\u0635\u0635 \u0644\u0625\u062c\u0631\u0627\u0621 \u0627\u0644\u0646\u0642\u0631 \u0627\u0644\u0645\u0632\u062f\u0648\u062c \u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629" }, "auto_reply": { - "name": "الرد التلقائي", - "description": "يرسل ردوداً تلقائية على الرسائل الواردة عندما تكون بعيداً", + "name": "\u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u064a\u0631\u0633\u0644 \u0631\u062f\u0648\u062f\u0627\u064b \u062a\u0644\u0642\u0627\u0626\u064a\u0629 \u0639\u0644\u0649 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0648\u0627\u0631\u062f\u0629 \u0639\u0646\u062f\u0645\u0627 \u062a\u0643\u0648\u0646 \u0628\u0639\u064a\u062f\u0627\u064b", "properties": { "allow_running_in_background": { - "name": "السماح بالتشغيل في الخلفية", - "description": "يسمح للرد التلقائي بالعمل في الخلفية. ملاحظة: هذا سيستنزف بطاريتك بشكل كبير" + "name": "\u0627\u0644\u0633\u0645\u0627\u062d \u0628\u0627\u0644\u062a\u0634\u063a\u064a\u0644 \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629", + "description": "\u064a\u0633\u0645\u062d \u0644\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0628\u0627\u0644\u0639\u0645\u0644 \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629. \u0645\u0644\u0627\u062d\u0638\u0629: \u0647\u0630\u0627 \u0633\u064a\u0633\u062a\u0646\u0632\u0641 \u0628\u0637\u0627\u0631\u064a\u062a\u0643 \u0628\u0634\u0643\u0644 \u0643\u0628\u064a\u0631" }, "cooldown_seconds": { - "name": "ثواني التهدئة", - "description": "الحد الأدنى للوقت بين الردود التلقائية لنفس المحادثة (بالثواني)" + "name": "\u062b\u0648\u0627\u0646\u064a \u0627\u0644\u062a\u0647\u062f\u0626\u0629", + "description": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u062f\u0646\u0649 \u0644\u0644\u0648\u0642\u062a \u0628\u064a\u0646 \u0627\u0644\u0631\u062f\u0648\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a\u0629 \u0644\u0646\u0641\u0633 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629 (\u0628\u0627\u0644\u062b\u0648\u0627\u0646\u064a)" }, "message_age_threshold": { - "name": "عتبة عمر الرسالة", - "description": "الرد فقط على الرسائل المستلمة خلال هذا الإطار الزمني (بالثواني)" + "name": "\u0639\u062a\u0628\u0629 \u0639\u0645\u0631 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "description": "\u0627\u0644\u0631\u062f \u0641\u0642\u0637 \u0639\u0644\u0649 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0633\u062a\u0644\u0645\u0629 \u062e\u0644\u0627\u0644 \u0647\u0630\u0627 \u0627\u0644\u0625\u0637\u0627\u0631 \u0627\u0644\u0632\u0645\u0646\u064a (\u0628\u0627\u0644\u062b\u0648\u0627\u0646\u064a)" }, "ai_config": { - "name": "تكوين الذكاء الاصطناعي (AI)", - "description": "إعدادات الردود التلقائية المدعومة بالذكاء الاصطناعي", + "name": "\u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u0630\u0643\u0627\u0621 \u0627\u0644\u0627\u0635\u0637\u0646\u0627\u0639\u064a (AI)", + "description": "\u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u0631\u062f\u0648\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a\u0629 \u0627\u0644\u0645\u062f\u0639\u0648\u0645\u0629 \u0628\u0627\u0644\u0630\u0643\u0627\u0621 \u0627\u0644\u0627\u0635\u0637\u0646\u0627\u0639\u064a", "properties": { "enable_ai_replies": { - "name": "تمكين ردود AI", - "description": "استخدم AI لتوليد ردود تلقائية ذكية بدلاً من رسائل القوالب" + "name": "\u062a\u0645\u0643\u064a\u0646 \u0631\u062f\u0648\u062f AI", + "description": "\u0627\u0633\u062a\u062e\u062f\u0645 AI \u0644\u062a\u0648\u0644\u064a\u062f \u0631\u062f\u0648\u062f \u062a\u0644\u0642\u0627\u0626\u064a\u0629 \u0630\u0643\u064a\u0629 \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0642\u0648\u0627\u0644\u0628" }, "ai_provider": { - "name": "مزود AI", - "description": "حدد خدمة AI لاستخدامها لتوليد الردود" + "name": "\u0645\u0632\u0648\u062f AI", + "description": "\u062d\u062f\u062f \u062e\u062f\u0645\u0629 AI \u0644\u0627\u0633\u062a\u062e\u062f\u0627\u0645\u0647\u0627 \u0644\u062a\u0648\u0644\u064a\u062f \u0627\u0644\u0631\u062f\u0648\u062f" }, "ai_endpoint_url": { - "name": "رابط نقطة نهاية AI", - "description": "رابط نقطة نهاية API لخدمة AI (مثل OpenAI، خادم AI محلي)" + "name": "\u0631\u0627\u0628\u0637 \u0646\u0642\u0637\u0629 \u0646\u0647\u0627\u064a\u0629 AI", + "description": "\u0631\u0627\u0628\u0637 \u0646\u0642\u0637\u0629 \u0646\u0647\u0627\u064a\u0629 API \u0644\u062e\u062f\u0645\u0629 AI (\u0645\u062b\u0644 OpenAI\u060c \u062e\u0627\u062f\u0645 AI \u0645\u062d\u0644\u064a)" }, "ai_model": { - "name": "نموذج AI", - "description": "نموذج AI المستخدم لتوليد الردود (مثل gpt-3.5-turbo, gpt-4)" + "name": "\u0646\u0645\u0648\u0630\u062c AI", + "description": "\u0646\u0645\u0648\u0630\u062c AI \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0644\u062a\u0648\u0644\u064a\u062f \u0627\u0644\u0631\u062f\u0648\u062f (\u0645\u062b\u0644 gpt-3.5-turbo, gpt-4)" }, "ai_api_key": { - "name": "مفتاح API لـ AI", - "description": "مفتاح API للمصادقة مع خدمة AI" + "name": "\u0645\u0641\u062a\u0627\u062d API \u0644\u0640 AI", + "description": "\u0645\u0641\u062a\u0627\u062d API \u0644\u0644\u0645\u0635\u0627\u062f\u0642\u0629 \u0645\u0639 \u062e\u062f\u0645\u0629 AI" }, "ai_system_prompt": { - "name": "موجه النظام لـ AI", - "description": "موجه النظام الذي يحدد شخصية وسلوك AI" + "name": "\u0645\u0648\u062c\u0647 \u0627\u0644\u0646\u0638\u0627\u0645 \u0644\u0640 AI", + "description": "\u0645\u0648\u062c\u0647 \u0627\u0644\u0646\u0638\u0627\u0645 \u0627\u0644\u0630\u064a \u064a\u062d\u062f\u062f \u0634\u062e\u0635\u064a\u0629 \u0648\u0633\u0644\u0648\u0643 AI" }, "ai_max_tokens": { - "name": "الحد الأقصى لرموز AI", - "description": "الحد الأقصى لعدد الرموز (الكلمات) التي يمكن لـ AI استخدامها في الردود" + "name": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0631\u0645\u0648\u0632 AI", + "description": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0639\u062f\u062f \u0627\u0644\u0631\u0645\u0648\u0632 (\u0627\u0644\u0643\u0644\u0645\u0627\u062a) \u0627\u0644\u062a\u064a \u064a\u0645\u0643\u0646 \u0644\u0640 AI \u0627\u0633\u062a\u062e\u062f\u0627\u0645\u0647\u0627 \u0641\u064a \u0627\u0644\u0631\u062f\u0648\u062f" }, "ai_temperature": { - "name": "درجة حرارة AI", - "description": "تتحكم في العشوائية في ردود AI (0.0 = حتمي، 2.0 = عشوائي جداً)" + "name": "\u062f\u0631\u062c\u0629 \u062d\u0631\u0627\u0631\u0629 AI", + "description": "\u062a\u062a\u062d\u0643\u0645 \u0641\u064a \u0627\u0644\u0639\u0634\u0648\u0627\u0626\u064a\u0629 \u0641\u064a \u0631\u062f\u0648\u062f AI (0.0 = \u062d\u062a\u0645\u064a\u060c 2.0 = \u0639\u0634\u0648\u0627\u0626\u064a \u062c\u062f\u0627\u064b)" }, "ai_context_length": { - "name": "طول سياق AI", - "description": "عدد الرسائل السابقة لتضمينها كسياق لردود AI" + "name": "\u0637\u0648\u0644 \u0633\u064a\u0627\u0642 AI", + "description": "\u0639\u062f\u062f \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0633\u0627\u0628\u0642\u0629 \u0644\u062a\u0636\u0645\u064a\u0646\u0647\u0627 \u0643\u0633\u064a\u0627\u0642 \u0644\u0631\u062f\u0648\u062f AI" }, "ai_personality_traits": { - "name": "سمات شخصية AI", - "description": "سمات شخصية مفصولة بفواصل لـ AI (مثل: ودود، عفوي، مفيد)" + "name": "\u0633\u0645\u0627\u062a \u0634\u062e\u0635\u064a\u0629 AI", + "description": "\u0633\u0645\u0627\u062a \u0634\u062e\u0635\u064a\u0629 \u0645\u0641\u0635\u0648\u0644\u0629 \u0628\u0641\u0648\u0627\u0635\u0644 \u0644\u0640 AI (\u0645\u062b\u0644: \u0648\u062f\u0648\u062f\u060c \u0639\u0641\u0648\u064a\u060c \u0645\u0641\u064a\u062f)" }, "ai_response_style": { - "name": "أسلوب رد AI", - "description": "الأسلوب العام لردود AI" + "name": "\u0623\u0633\u0644\u0648\u0628 \u0631\u062f AI", + "description": "\u0627\u0644\u0623\u0633\u0644\u0648\u0628 \u0627\u0644\u0639\u0627\u0645 \u0644\u0631\u062f\u0648\u062f AI" }, "ai_response_language": { - "name": "لغة رد AI", - "description": "لغة ردود AI (تلقائي = نفس الرسالة المستلمة)" + "name": "\u0644\u063a\u0629 \u0631\u062f AI", + "description": "\u0644\u063a\u0629 \u0631\u062f\u0648\u062f AI (\u062a\u0644\u0642\u0627\u0626\u064a = \u0646\u0641\u0633 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u0627\u0644\u0645\u0633\u062a\u0644\u0645\u0629)" }, "ai_use_conversation_history": { - "name": "استخدام تاريخ المحادثة", - "description": "تضمين الرسائل السابقة كسياق لردود AI أكثر ملاءمة" + "name": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "description": "\u062a\u0636\u0645\u064a\u0646 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0633\u0627\u0628\u0642\u0629 \u0643\u0633\u064a\u0627\u0642 \u0644\u0631\u062f\u0648\u062f AI \u0623\u0643\u062b\u0631 \u0645\u0644\u0627\u0621\u0645\u0629" }, "ai_include_friend_info": { - "name": "تضمين معلومات الصديق", - "description": "تضمين اسم الصديق ومعلومات أخرى متاحة في سياق AI" + "name": "\u062a\u0636\u0645\u064a\u0646 \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u0635\u062f\u064a\u0642", + "description": "\u062a\u0636\u0645\u064a\u0646 \u0627\u0633\u0645 \u0627\u0644\u0635\u062f\u064a\u0642 \u0648\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649 \u0645\u062a\u0627\u062d\u0629 \u0641\u064a \u0633\u064a\u0627\u0642 AI" }, "ai_fallback_to_template": { - "name": "العودة إلى القالب", - "description": "استخدام رسائل القالب إذا فشل AI في توليد رد" + "name": "\u0627\u0644\u0639\u0648\u062f\u0629 \u0625\u0644\u0649 \u0627\u0644\u0642\u0627\u0644\u0628", + "description": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0642\u0627\u0644\u0628 \u0625\u0630\u0627 \u0641\u0634\u0644 AI \u0641\u064a \u062a\u0648\u0644\u064a\u062f \u0631\u062f" }, "ai_request_timeout": { - "name": "مهلة طلب AI", - "description": "الحد الأقصى للوقت لانتظار رد AI (بالثواني)" + "name": "\u0645\u0647\u0644\u0629 \u0637\u0644\u0628 AI", + "description": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0644\u0648\u0642\u062a \u0644\u0627\u0646\u062a\u0638\u0627\u0631 \u0631\u062f AI (\u0628\u0627\u0644\u062b\u0648\u0627\u0646\u064a)" }, "ai_retry_attempts": { - "name": "محاولات إعادة AI", - "description": "عدد المرات لإعادة محاولة طلبات AI إذا فشلت" + "name": "\u0645\u062d\u0627\u0648\u0644\u0627\u062a \u0625\u0639\u0627\u062f\u0629 AI", + "description": "\u0639\u062f\u062f \u0627\u0644\u0645\u0631\u0627\u062a \u0644\u0625\u0639\u0627\u062f\u0629 \u0645\u062d\u0627\u0648\u0644\u0629 \u0637\u0644\u0628\u0627\u062a AI \u0625\u0630\u0627 \u0641\u0634\u0644\u062a" } } }, "auto_trigger_config": { - "name": "تكوين المحفز التلقائي", - "description": "إعدادات لمحفزات الرد التلقائي وقوالب الرسائل", + "name": "\u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u0645\u062d\u0641\u0632 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0644\u0645\u062d\u0641\u0632\u0627\u062a \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0648\u0642\u0648\u0627\u0644\u0628 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", "properties": { "friendSpecificGreeting": { - "name": "تحية خاصة بالصديق", - "description": "إضافة اسم الصديق للردود التلقائية للتخصيص" + "name": "\u062a\u062d\u064a\u0629 \u062e\u0627\u0635\u0629 \u0628\u0627\u0644\u0635\u062f\u064a\u0642", + "description": "\u0625\u0636\u0627\u0641\u0629 \u0627\u0633\u0645 \u0627\u0644\u0635\u062f\u064a\u0642 \u0644\u0644\u0631\u062f\u0648\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a\u0629 \u0644\u0644\u062a\u062e\u0635\u064a\u0635" }, "friendGreeting": { - "name": "تحية الصديق", - "description": "نص التحية لاستخدامه عند تمكين التحية الخاصة بالصديق" + "name": "\u062a\u062d\u064a\u0629 \u0627\u0644\u0635\u062f\u064a\u0642", + "description": "\u0646\u0635 \u0627\u0644\u062a\u062d\u064a\u0629 \u0644\u0627\u0633\u062a\u062e\u062f\u0627\u0645\u0647 \u0639\u0646\u062f \u062a\u0645\u0643\u064a\u0646 \u0627\u0644\u062a\u062d\u064a\u0629 \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0627\u0644\u0635\u062f\u064a\u0642" }, "auto_reply_content_types": { - "name": "محفزات الرد التلقائي", - "description": "حدد أنواع الرسائل التي يجب أن تحفز الردود التلقائية" + "name": "\u0645\u062d\u0641\u0632\u0627\u062a \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u062d\u062f\u062f \u0623\u0646\u0648\u0627\u0639 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062a\u064a \u064a\u062c\u0628 \u0623\u0646 \u062a\u062d\u0641\u0632 \u0627\u0644\u0631\u062f\u0648\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a\u0629" }, "chat_messages": { - "name": "ردود رسائل الدردشة", - "description": "رسائل الرد التلقائي لرسائل الدردشة النصية" + "name": "\u0631\u062f\u0648\u062f \u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "description": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062f\u0631\u062f\u0634\u0629 \u0627\u0644\u0646\u0635\u064a\u0629" }, "snap_messages": { - "name": "ردود Snaps", - "description": "رسائل الرد التلقائي للـ snaps" + "name": "\u0631\u062f\u0648\u062f Snaps", + "description": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0644\u0640 snaps" }, "story_share_messages": { - "name": "ردود مشاركة القصة", - "description": "رسائل الرد التلقائي لمشاركات القصة" + "name": "\u0631\u062f\u0648\u062f \u0645\u0634\u0627\u0631\u0643\u0629 \u0627\u0644\u0642\u0635\u0629", + "description": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0645\u0634\u0627\u0631\u0643\u0627\u062a \u0627\u0644\u0642\u0635\u0629" }, "story_reply_messages": { - "name": "ردود رد القصة", - "description": "رسائل الرد التلقائي لردود القصة" + "name": "\u0631\u062f\u0648\u062f \u0631\u062f \u0627\u0644\u0642\u0635\u0629", + "description": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0631\u062f\u0648\u062f \u0627\u0644\u0642\u0635\u0629" }, "external_media_messages": { - "name": "ردود الوسائط الخارجية", - "description": "رسائل الرد التلقائي للوسائط الخارجية" + "name": "\u0631\u062f\u0648\u062f \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u062e\u0627\u0631\u062c\u064a\u0629", + "description": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u062e\u0627\u0631\u062c\u064a\u0629" }, "voice_note_messages": { - "name": "ردود الملاحظات الصوتية", - "description": "رسائل الرد التلقائي للملاحظات الصوتية" + "name": "\u0631\u062f\u0648\u062f \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629", + "description": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629" }, "sticker_messages": { - "name": "ردود الملصقات", - "description": "رسائل الرد التلقائي للملصقات" + "name": "\u0631\u062f\u0648\u062f \u0627\u0644\u0645\u0644\u0635\u0642\u0627\u062a", + "description": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0644\u0645\u0644\u0635\u0642\u0627\u062a" }, "tiny_snap_messages": { - "name": "ردود Tiny Snap", - "description": "رسائل الرد التلقائي للـ tiny snaps" + "name": "\u0631\u062f\u0648\u062f Tiny Snap", + "description": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0644\u0640 tiny snaps" }, "map_reaction_messages": { - "name": "ردود تفاعلات الخريطة", - "description": "رسائل الرد التلقائي لتفاعلات الخريطة" + "name": "\u0631\u062f\u0648\u062f \u062a\u0641\u0627\u0639\u0644\u0627\u062a \u0627\u0644\u062e\u0631\u064a\u0637\u0629", + "description": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0644\u062a\u0641\u0627\u0639\u0644\u0627\u062a \u0627\u0644\u062e\u0631\u064a\u0637\u0629" }, "half_swipe_messages": { - "name": "رسائل التمرير النصفي", - "description": "رسائل الرد التلقائي للتمرير النصفي" + "name": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a", + "description": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a" } } } } }, "auto_open_snaps": { - "name": "إعدادات فتح الـ Snaps تلقائياً", - "description": "تكوين إعدادات التأخير وقائمة الانتظار لفتح الـ Snaps تلقائياً", + "name": "\u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0641\u062a\u062d \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "description": "\u062a\u0643\u0648\u064a\u0646 \u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u062a\u0623\u062e\u064a\u0631 \u0648\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631 \u0644\u0641\u062a\u062d \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", "properties": { "allow_running_in_background": { - "name": "السماح بالتشغيل في الخلفية", - "description": "يسمح لفتح الـ Snaps تلقائياً بالعمل في الخلفية. ملاحظة: هذا سيستنزف بطاريتك بشكل كبير" + "name": "\u0627\u0644\u0633\u0645\u0627\u062d \u0628\u0627\u0644\u062a\u0634\u063a\u064a\u0644 \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629", + "description": "\u064a\u0633\u0645\u062d \u0644\u0641\u062a\u062d \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0628\u0627\u0644\u0639\u0645\u0644 \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629. \u0645\u0644\u0627\u062d\u0638\u0629: \u0647\u0630\u0627 \u0633\u064a\u0633\u062a\u0646\u0632\u0641 \u0628\u0637\u0627\u0631\u064a\u062a\u0643 \u0628\u0634\u0643\u0644 \u0643\u0628\u064a\u0631" }, "min_delay": { - "name": "الحد الأدنى للتأخير (مللي ثانية)", - "description": "الحد الأدنى للتأخير بالمللي ثانية قبل فتح snap" + "name": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u062f\u0646\u0649 \u0644\u0644\u062a\u0623\u062e\u064a\u0631 (\u0645\u0644\u0644\u064a \u062b\u0627\u0646\u064a\u0629)", + "description": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u062f\u0646\u0649 \u0644\u0644\u062a\u0623\u062e\u064a\u0631 \u0628\u0627\u0644\u0645\u0644\u0644\u064a \u062b\u0627\u0646\u064a\u0629 \u0642\u0628\u0644 \u0641\u062a\u062d snap" }, "max_delay_ms": { - "name": "الحد الأقصى للتأخير (مللي ثانية)", - "description": "الحد الأقصى للتأخير بالمللي ثانية قبل فتح snap" + "name": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0644\u062a\u0623\u062e\u064a\u0631 (\u0645\u0644\u0644\u064a \u062b\u0627\u0646\u064a\u0629)", + "description": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0644\u062a\u0623\u062e\u064a\u0631 \u0628\u0627\u0644\u0645\u0644\u0644\u064a \u062b\u0627\u0646\u064a\u0629 \u0642\u0628\u0644 \u0641\u062a\u062d snap" }, "queue_size": { - "name": "حجم قائمة الانتظار", - "description": "الحد الأقصى لعدد الـ snaps للاحتفاظ بها في قائمة الانتظار" + "name": "\u062d\u062c\u0645 \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631", + "description": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0639\u062f\u062f \u0627\u0644\u0640 snaps \u0644\u0644\u0627\u062d\u062a\u0641\u0627\u0638 \u0628\u0647\u0627 \u0641\u064a \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631" }, "retry_attempts": { - "name": "محاولات الإعادة", - "description": "عدد المرات لإعادة محاولة فتح snap إذا فشل" + "name": "\u0645\u062d\u0627\u0648\u0644\u0627\u062a \u0627\u0644\u0625\u0639\u0627\u062f\u0629", + "description": "\u0639\u062f\u062f \u0627\u0644\u0645\u0631\u0627\u062a \u0644\u0625\u0639\u0627\u062f\u0629 \u0645\u062d\u0627\u0648\u0644\u0629 \u0641\u062a\u062d snap \u0625\u0630\u0627 \u0641\u0634\u0644" }, "retry_delay": { - "name": "تأخير الإعادة (مللي ثانية)", - "description": "التأخير بالمللي ثانية بين محاولات الإعادة" + "name": "\u062a\u0623\u062e\u064a\u0631 \u0627\u0644\u0625\u0639\u0627\u062f\u0629 (\u0645\u0644\u0644\u064a \u062b\u0627\u0646\u064a\u0629)", + "description": "\u0627\u0644\u062a\u0623\u062e\u064a\u0631 \u0628\u0627\u0644\u0645\u0644\u0644\u064a \u062b\u0627\u0646\u064a\u0629 \u0628\u064a\u0646 \u0645\u062d\u0627\u0648\u0644\u0627\u062a \u0627\u0644\u0625\u0639\u0627\u062f\u0629" } } }, "auto_delete_sent_messages": { - "name": "حذف الرسائل المرسلة تلقائياً", - "description": "يحذف الرسائل المرسلة تلقائياً بعد فترة زمنية محددة", + "name": "\u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0631\u0633\u0644\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "description": "\u064a\u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0631\u0633\u0644\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0628\u0639\u062f \u0641\u062a\u0631\u0629 \u0632\u0645\u0646\u064a\u0629 \u0645\u062d\u062f\u062f\u0629", "properties": { "allow_running_in_background": { - "name": "السماح بالتشغيل في الخلفية", - "description": "يسمح لحذف الرسائل المرسلة تلقائياً بالعمل في الخلفية. ملاحظة: هذا سيستنزف بطاريتك بشكل كبير" + "name": "\u0627\u0644\u0633\u0645\u0627\u062d \u0628\u0627\u0644\u062a\u0634\u063a\u064a\u0644 \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629", + "description": "\u064a\u0633\u0645\u062d \u0644\u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0631\u0633\u0644\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0628\u0627\u0644\u0639\u0645\u0644 \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629. \u0645\u0644\u0627\u062d\u0638\u0629: \u0647\u0630\u0627 \u0633\u064a\u0633\u062a\u0646\u0632\u0641 \u0628\u0637\u0627\u0631\u064a\u062a\u0643 \u0628\u0634\u0643\u0644 \u0643\u0628\u064a\u0631" }, "delete_after_value": { - "name": "حذف بعد (قيمة)", - "description": "قيمة الوقت قبل حذف الرسالة المرسلة" + "name": "\u062d\u0630\u0641 \u0628\u0639\u062f (\u0642\u064a\u0645\u0629)", + "description": "\u0642\u064a\u0645\u0629 \u0627\u0644\u0648\u0642\u062a \u0642\u0628\u0644 \u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u0627\u0644\u0645\u0631\u0633\u0644\u0629" }, "delete_after_unit": { - "name": "وحدة الوقت", - "description": "حدد وحدة الوقت لتأخير الحذف" + "name": "\u0648\u062d\u062f\u0629 \u0627\u0644\u0648\u0642\u062a", + "description": "\u062d\u062f\u062f \u0648\u062d\u062f\u0629 \u0627\u0644\u0648\u0642\u062a \u0644\u062a\u0623\u062e\u064a\u0631 \u0627\u0644\u062d\u0630\u0641" }, "message_types": { - "name": "أنواع الرسائل", - "description": "حدد أنواع الرسائل التي يجب حذفها تلقائياً" + "name": "\u0623\u0646\u0648\u0627\u0639 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "description": "\u062d\u062f\u062f \u0623\u0646\u0648\u0627\u0639 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062a\u064a \u064a\u062c\u0628 \u062d\u0630\u0641\u0647\u0627 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b" }, "show_countdown": { - "name": "إظهار العد التنازلي", - "description": "إظهار العد التنازلي قبل حذف الرسالة" + "name": "\u0625\u0638\u0647\u0627\u0631 \u0627\u0644\u0639\u062f \u0627\u0644\u062a\u0646\u0627\u0632\u0644\u064a", + "description": "\u0625\u0638\u0647\u0627\u0631 \u0627\u0644\u0639\u062f \u0627\u0644\u062a\u0646\u0627\u0632\u0644\u064a \u0642\u0628\u0644 \u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0644\u0629" }, "show_notification": { - "name": "إظهار الإشعار", - "description": "إظهار الإشعار أثناء العد التنازلي" + "name": "\u0625\u0638\u0647\u0627\u0631 \u0627\u0644\u0625\u0634\u0639\u0627\u0631", + "description": "\u0625\u0638\u0647\u0627\u0631 \u0627\u0644\u0625\u0634\u0639\u0627\u0631 \u0623\u062b\u0646\u0627\u0621 \u0627\u0644\u0639\u062f \u0627\u0644\u062a\u0646\u0627\u0632\u0644\u064a" } } }, "instant_translation": { - "name": "مترجم الرسائل", - "description": "ترجمة الرسائل الواردة تلقائياً إلى لغتك المفضلة", + "name": "\u0645\u062a\u0631\u062c\u0645 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "description": "\u062a\u0631\u062c\u0645\u0629 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0648\u0627\u0631\u062f\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0625\u0644\u0649 \u0644\u063a\u062a\u0643 \u0627\u0644\u0645\u0641\u0636\u0644\u0629", "properties": { "enabled": { - "name": "تمكين مترجم الرسائل", - "description": "تمكين الترجمة التلقائية للرسائل" + "name": "\u062a\u0645\u0643\u064a\u0646 \u0645\u062a\u0631\u062c\u0645 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "description": "\u062a\u0645\u0643\u064a\u0646 \u0627\u0644\u062a\u0631\u062c\u0645\u0629 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a\u0629 \u0644\u0644\u0631\u0633\u0627\u0626\u0644" }, "source_language": { - "name": "لغة المصدر", - "description": "اللغة للترجمة منها (استخدم 'تلقائي' للكشف التلقائي)" + "name": "\u0644\u063a\u0629 \u0627\u0644\u0645\u0635\u062f\u0631", + "description": "\u0627\u0644\u0644\u063a\u0629 \u0644\u0644\u062a\u0631\u062c\u0645\u0629 \u0645\u0646\u0647\u0627 (\u0627\u0633\u062a\u062e\u062f\u0645 '\u062a\u0644\u0642\u0627\u0626\u064a' \u0644\u0644\u0643\u0634\u0641 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a)" }, "target_language": { - "name": "اللغة الهدف", - "description": "اللغة للترجمة إليها" + "name": "\u0627\u0644\u0644\u063a\u0629 \u0627\u0644\u0647\u062f\u0641", + "description": "\u0627\u0644\u0644\u063a\u0629 \u0644\u0644\u062a\u0631\u062c\u0645\u0629 \u0625\u0644\u064a\u0647\u0627" }, "show_original": { - "name": "إظهار النص الأصلي", - "description": "عرض نص الرسالة الأصلي" + "name": "\u0625\u0638\u0647\u0627\u0631 \u0627\u0644\u0646\u0635 \u0627\u0644\u0623\u0635\u0644\u064a", + "description": "\u0639\u0631\u0636 \u0646\u0635 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u0627\u0644\u0623\u0635\u0644\u064a" }, "show_translation": { - "name": "إظهار الترجمة", - "description": "عرض النص المترجم" + "name": "\u0625\u0638\u0647\u0627\u0631 \u0627\u0644\u062a\u0631\u062c\u0645\u0629", + "description": "\u0639\u0631\u0636 \u0627\u0644\u0646\u0635 \u0627\u0644\u0645\u062a\u0631\u062c\u0645" }, "translation_position": { - "name": "موضع الترجمة", - "description": "مكان عرض الترجمة بالنسبة للنص الأصلي" + "name": "\u0645\u0648\u0636\u0639 \u0627\u0644\u062a\u0631\u062c\u0645\u0629", + "description": "\u0645\u0643\u0627\u0646 \u0639\u0631\u0636 \u0627\u0644\u062a\u0631\u062c\u0645\u0629 \u0628\u0627\u0644\u0646\u0633\u0628\u0629 \u0644\u0644\u0646\u0635 \u0627\u0644\u0623\u0635\u0644\u064a" }, "auto_translate": { - "name": "ترجمة تلقائية", - "description": "ترجمة الرسائل تلقائياً عند استلامها" + "name": "\u062a\u0631\u062c\u0645\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0629", + "description": "\u062a\u0631\u062c\u0645\u0629 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0639\u0646\u062f \u0627\u0633\u062a\u0644\u0627\u0645\u0647\u0627" }, "translate_on_tap": { - "name": "ترجمة عند النقر", - "description": "ترجمة الرسائل عند النقر عليها" + "name": "\u062a\u0631\u062c\u0645\u0629 \u0639\u0646\u062f \u0627\u0644\u0646\u0642\u0631", + "description": "\u062a\u0631\u062c\u0645\u0629 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0639\u0646\u062f \u0627\u0644\u0646\u0642\u0631 \u0639\u0644\u064a\u0647\u0627" }, "supported_languages": { - "name": "اللغات المدعومة", - "description": "اللغات المتاحة للترجمة" + "name": "\u0627\u0644\u0644\u063a\u0627\u062a \u0627\u0644\u0645\u062f\u0639\u0648\u0645\u0629", + "description": "\u0627\u0644\u0644\u063a\u0627\u062a \u0627\u0644\u0645\u062a\u0627\u062d\u0629 \u0644\u0644\u062a\u0631\u062c\u0645\u0629" }, "pause_on_error": { - "name": "إيقاف مؤقت عند الخطأ", - "description": "إيقاف الترجمة مؤقتاً عند حظر الخدمة" + "name": "\u0625\u064a\u0642\u0627\u0641 \u0645\u0624\u0642\u062a \u0639\u0646\u062f \u0627\u0644\u062e\u0637\u0623", + "description": "\u0625\u064a\u0642\u0627\u0641 \u0627\u0644\u062a\u0631\u062c\u0645\u0629 \u0645\u0624\u0642\u062a\u0627\u064b \u0639\u0646\u062f \u062d\u0638\u0631 \u0627\u0644\u062e\u062f\u0645\u0629" }, "max_retries": { - "name": "الحد الأقصى للمحاولات", - "description": "الحد الأقصى لمحاولات الإعادة" + "name": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0644\u0645\u062d\u0627\u0648\u0644\u0627\u062a", + "description": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0645\u062d\u0627\u0648\u0644\u0627\u062a \u0627\u0644\u0625\u0639\u0627\u062f\u0629" }, "retry_delay": { - "name": "تأخير الإعادة", - "description": "التأخير بين محاولات الإعادة (مللي ثانية)" + "name": "\u062a\u0623\u062e\u064a\u0631 \u0627\u0644\u0625\u0639\u0627\u062f\u0629", + "description": "\u0627\u0644\u062a\u0623\u062e\u064a\u0631 \u0628\u064a\u0646 \u0645\u062d\u0627\u0648\u0644\u0627\u062a \u0627\u0644\u0625\u0639\u0627\u062f\u0629 (\u0645\u0644\u0644\u064a \u062b\u0627\u0646\u064a\u0629)" } } }, "scheduled_send_allow_running_in_background": { - "name": "السماح للإرسال المجدول بالعمل في الخلفية", - "description": "الحفاظ على معالجة الرسائل المجدولة أثناء وجود Snapchat في الخلفية" + "name": "\u0627\u0644\u0633\u0645\u0627\u062d \u0644\u0644\u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0645\u062c\u062f\u0648\u0644 \u0628\u0627\u0644\u0639\u0645\u0644 \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629", + "description": "\u0627\u0644\u062d\u0641\u0627\u0638 \u0639\u0644\u0649 \u0645\u0639\u0627\u0644\u062c\u0629 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u062c\u062f\u0648\u0644\u0629 \u0623\u062b\u0646\u0627\u0621 \u0648\u062c\u0648\u062f Snapchat \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629" } } }, "global": { - "name": "عالمي", - "description": "تعديل إعدادات Snapchat العالمية", + "name": "\u0639\u0627\u0644\u0645\u064a", + "description": "\u062a\u0639\u062f\u064a\u0644 \u0625\u0639\u062f\u0627\u062f\u0627\u062a Snapchat \u0627\u0644\u0639\u0627\u0644\u0645\u064a\u0629", "properties": { "ui_settings": { - "name": "إعدادات واجهة المستخدم", - "description": "ضبط سلوك الملاحظات والرسائل المنبثقة", + "name": "\u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0648\u0627\u062c\u0647\u0629 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "description": "\u0636\u0628\u0637 \u0633\u0644\u0648\u0643 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0648\u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0646\u0628\u062b\u0642\u0629", "properties": { "haptic_feedback": { - "name": "الاستجابة اللمسية", - "description": "الاهتزاز عند التفاعلات المدعومة" + "name": "\u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0627\u0644\u0644\u0645\u0633\u064a\u0629", + "description": "\u0627\u0644\u0627\u0647\u062a\u0632\u0627\u0632 \u0639\u0646\u062f \u0627\u0644\u062a\u0641\u0627\u0639\u0644\u0627\u062a \u0627\u0644\u0645\u062f\u0639\u0648\u0645\u0629" }, "use_system_toasts": { - "name": "استخدام رسائل النظام المنبثقة (Toasts)", - "description": "عرض رسائل النظام المنبثقة بدلاً من التراكبات داخل التطبيق" + "name": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0646\u0638\u0627\u0645 \u0627\u0644\u0645\u0646\u0628\u062b\u0642\u0629 (Toasts)", + "description": "\u0639\u0631\u0636 \u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0646\u0638\u0627\u0645 \u0627\u0644\u0645\u0646\u0628\u062b\u0642\u0629 \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0627\u0644\u062a\u0631\u0627\u0643\u0628\u0627\u062a \u062f\u0627\u062e\u0644 \u0627\u0644\u062a\u0637\u0628\u064a\u0642" } } }, "update_settings": { - "name": "إعدادات التحديث", - "description": "التحكم في فحوصات التحديث التلقائي", + "name": "\u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u062a\u062d\u062f\u064a\u062b", + "description": "\u0627\u0644\u062a\u062d\u0643\u0645 \u0641\u064a \u0641\u062d\u0648\u0635\u0627\u062a \u0627\u0644\u062a\u062d\u062f\u064a\u062b \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", "properties": { "auto_update_check": { - "name": "فحص التحديث التلقائي", - "description": "التحقق من الإصدارات الجديدة تلقائياً" + "name": "\u0641\u062d\u0635 \u0627\u0644\u062a\u062d\u062f\u064a\u062b \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0625\u0635\u062f\u0627\u0631\u0627\u062a \u0627\u0644\u062c\u062f\u064a\u062f\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b" }, "update_check_frequency": { - "name": "تكرار فحص التحديث", - "description": "كم مرة يتم التحقق من التحديثات" + "name": "\u062a\u0643\u0631\u0627\u0631 \u0641\u062d\u0635 \u0627\u0644\u062a\u062d\u062f\u064a\u062b", + "description": "\u0643\u0645 \u0645\u0631\u0629 \u064a\u062a\u0645 \u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u062a\u062d\u062f\u064a\u062b\u0627\u062a" } } }, "better_location": { - "name": "موقع أفضل", - "description": "يحسن موقع Snapchat", + "name": "\u0645\u0648\u0642\u0639 \u0623\u0641\u0636\u0644", + "description": "\u064a\u062d\u0633\u0646 \u0645\u0648\u0642\u0639 Snapchat", "properties": { "spoof_location": { - "name": "تزييف الموقع", - "description": "يزيف موقعك إلى موقع محدد" + "name": "\u062a\u0632\u064a\u064a\u0641 \u0627\u0644\u0645\u0648\u0642\u0639", + "description": "\u064a\u0632\u064a\u0641 \u0645\u0648\u0642\u0639\u0643 \u0625\u0644\u0649 \u0645\u0648\u0642\u0639 \u0645\u062d\u062f\u062f" }, "location_search_provider": { - "name": "موفر البحث عن الموقع", - "description": "اختر الموفر للبحث عن المواقع" + "name": "\u0645\u0648\u0641\u0631 \u0627\u0644\u0628\u062d\u062b \u0639\u0646 \u0627\u0644\u0645\u0648\u0642\u0639", + "description": "\u0627\u062e\u062a\u0631 \u0627\u0644\u0645\u0648\u0641\u0631 \u0644\u0644\u0628\u062d\u062b \u0639\u0646 \u0627\u0644\u0645\u0648\u0627\u0642\u0639" }, "google_maps_api_key": { - "name": "مفتاح Google Maps API", - "description": "مطلوب في حال استخدام موفر خرائط Google" + "name": "\u0645\u0641\u062a\u0627\u062d Google Maps API", + "description": "\u0645\u0637\u0644\u0648\u0628 \u0641\u064a \u062d\u0627\u0644 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0645\u0648\u0641\u0631 \u062e\u0631\u0627\u0626\u0637 Google" }, "coordinates": { - "name": "الإحداثيات", - "description": "تعيين إحداثيات الموقع المزيف" + "name": "\u0627\u0644\u0625\u062d\u062f\u0627\u062b\u064a\u0627\u062a", + "description": "\u062a\u0639\u064a\u064a\u0646 \u0625\u062d\u062f\u0627\u062b\u064a\u0627\u062a \u0627\u0644\u0645\u0648\u0642\u0639 \u0627\u0644\u0645\u0632\u064a\u0641" }, "walk_radius": { - "name": "نصف قطر المشي", - "description": "المشي عشوائياً ضمن هذا القطر (بالقدم)" + "name": "\u0646\u0635\u0641 \u0642\u0637\u0631 \u0627\u0644\u0645\u0634\u064a", + "description": "\u0627\u0644\u0645\u0634\u064a \u0639\u0634\u0648\u0627\u0626\u064a\u0627\u064b \u0636\u0645\u0646 \u0647\u0630\u0627 \u0627\u0644\u0642\u0637\u0631 (\u0628\u0627\u0644\u0642\u062f\u0645)" }, "always_update_location": { - "name": "تحديث الموقع دائماً", - "description": "فرض Snapchat على تحديث الموقع حتى إذا لم يتم استقبال بيانات GPS" + "name": "\u062a\u062d\u062f\u064a\u062b \u0627\u0644\u0645\u0648\u0642\u0639 \u062f\u0627\u0626\u0645\u0627\u064b", + "description": "\u0641\u0631\u0636 Snapchat \u0639\u0644\u0649 \u062a\u062d\u062f\u064a\u062b \u0627\u0644\u0645\u0648\u0642\u0639 \u062d\u062a\u0649 \u0625\u0630\u0627 \u0644\u0645 \u064a\u062a\u0645 \u0627\u0633\u062a\u0642\u0628\u0627\u0644 \u0628\u064a\u0627\u0646\u0627\u062a GPS" }, "suspend_location_updates": { - "name": "تعليق تحديثات الموقع", - "description": "يمنع تحديث موقعك" + "name": "\u062a\u0639\u0644\u064a\u0642 \u062a\u062d\u062f\u064a\u062b\u0627\u062a \u0627\u0644\u0645\u0648\u0642\u0639", + "description": "\u064a\u0645\u0646\u0639 \u062a\u062d\u062f\u064a\u062b \u0645\u0648\u0642\u0639\u0643" }, "spoof_battery_level": { - "name": "تزييف مستوى البطارية", - "description": "يزيف مستوى بطارية جهازك على الخريطة\nيجب أن تكون القيمة بين 0 و 100" + "name": "\u062a\u0632\u064a\u064a\u0641 \u0645\u0633\u062a\u0648\u0649 \u0627\u0644\u0628\u0637\u0627\u0631\u064a\u0629", + "description": "\u064a\u0632\u064a\u0641 \u0645\u0633\u062a\u0648\u0649 \u0628\u0637\u0627\u0631\u064a\u0629 \u062c\u0647\u0627\u0632\u0643 \u0639\u0644\u0649 \u0627\u0644\u062e\u0631\u064a\u0637\u0629\n\u064a\u062c\u0628 \u0623\u0646 \u062a\u0643\u0648\u0646 \u0627\u0644\u0642\u064a\u0645\u0629 \u0628\u064a\u0646 0 \u0648 100" }, "spoof_headphones": { - "name": "تزييف سماعات الرأس", - "description": "يزيف حالة الاستماع للموسيقى على الخريطة" + "name": "\u062a\u0632\u064a\u064a\u0641 \u0633\u0645\u0627\u0639\u0627\u062a \u0627\u0644\u0631\u0623\u0633", + "description": "\u064a\u0632\u064a\u0641 \u062d\u0627\u0644\u0629 \u0627\u0644\u0627\u0633\u062a\u0645\u0627\u0639 \u0644\u0644\u0645\u0648\u0633\u064a\u0642\u0649 \u0639\u0644\u0649 \u0627\u0644\u062e\u0631\u064a\u0637\u0629" }, "show_battery_level": { - "name": "عرض مستوى البطارية", - "description": "يعرض مستوى بطارية أصدقائك على الخريطة" + "name": "\u0639\u0631\u0636 \u0645\u0633\u062a\u0648\u0649 \u0627\u0644\u0628\u0637\u0627\u0631\u064a\u0629", + "description": "\u064a\u0639\u0631\u0636 \u0645\u0633\u062a\u0648\u0649 \u0628\u0637\u0627\u0631\u064a\u0629 \u0623\u0635\u062f\u0642\u0627\u0626\u0643 \u0639\u0644\u0649 \u0627\u0644\u062e\u0631\u064a\u0637\u0629" } } }, "snapchat_plus": { "name": "Snapchat Plus", - "description": "يمكن ميزات Snapchat Plus\nبعض الميزات من جانب الخادم قد لا تعمل" + "description": "\u064a\u0645\u0643\u0646 \u0645\u064a\u0632\u0627\u062a Snapchat Plus\n\u0628\u0639\u0636 \u0627\u0644\u0645\u064a\u0632\u0627\u062a \u0645\u0646 \u062c\u0627\u0646\u0628 \u0627\u0644\u062e\u0627\u062f\u0645 \u0642\u062f \u0644\u0627 \u062a\u0639\u0645\u0644" }, "media_upload_quality": { - "name": "جودة رفع الوسائط", - "description": "يتجاوز جودة رفع الوسائط", + "name": "\u062c\u0648\u062f\u0629 \u0631\u0641\u0639 \u0627\u0644\u0648\u0633\u0627\u0626\u0637", + "description": "\u064a\u062a\u062c\u0627\u0648\u0632 \u062c\u0648\u062f\u0629 \u0631\u0641\u0639 \u0627\u0644\u0648\u0633\u0627\u0626\u0637", "properties": { "force_video_upload_source_quality": { - "name": "فرض جودة المصدر لرفع الفيديو", - "description": "يفرض على Snapchat استخدام جودة المصدر عند رفع الفيديوهات\nيرجى الملاحظة أن هذا قد لا يزيل البيانات الوصفية من الوسائط" + "name": "\u0641\u0631\u0636 \u062c\u0648\u062f\u0629 \u0627\u0644\u0645\u0635\u062f\u0631 \u0644\u0631\u0641\u0639 \u0627\u0644\u0641\u064a\u062f\u064a\u0648", + "description": "\u064a\u0641\u0631\u0636 \u0639\u0644\u0649 Snapchat \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u062c\u0648\u062f\u0629 \u0627\u0644\u0645\u0635\u062f\u0631 \u0639\u0646\u062f \u0631\u0641\u0639 \u0627\u0644\u0641\u064a\u062f\u064a\u0648\u0647\u0627\u062a\n\u064a\u0631\u062c\u0649 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0629 \u0623\u0646 \u0647\u0630\u0627 \u0642\u062f \u0644\u0627 \u064a\u0632\u064a\u0644 \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0648\u0635\u0641\u064a\u0629 \u0645\u0646 \u0627\u0644\u0648\u0633\u0627\u0626\u0637" }, "disable_image_compression": { - "name": "تعطيل ضغط الصور", - "description": "يعطل ضغط الصور عند رفع الوسائط" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0636\u063a\u0637 \u0627\u0644\u0635\u0648\u0631", + "description": "\u064a\u0639\u0637\u0644 \u0636\u063a\u0637 \u0627\u0644\u0635\u0648\u0631 \u0639\u0646\u062f \u0631\u0641\u0639 \u0627\u0644\u0648\u0633\u0627\u0626\u0637" }, "custom_image_upload_format": { - "name": "تنسيق رفع صور مخصص", - "description": "يعين تنسيق رفع صور مخصص\nحدد تنسيقاً بدون فقدان (مثل PNG) للحصول على أفضل جودة" + "name": "\u062a\u0646\u0633\u064a\u0642 \u0631\u0641\u0639 \u0635\u0648\u0631 \u0645\u062e\u0635\u0635", + "description": "\u064a\u0639\u064a\u0646 \u062a\u0646\u0633\u064a\u0642 \u0631\u0641\u0639 \u0635\u0648\u0631 \u0645\u062e\u0635\u0635\n\u062d\u062f\u062f \u062a\u0646\u0633\u064a\u0642\u0627\u064b \u0628\u062f\u0648\u0646 \u0641\u0642\u062f\u0627\u0646 (\u0645\u062b\u0644 PNG) \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0623\u0641\u0636\u0644 \u062c\u0648\u062f\u0629" } } }, "disable_confirmation_dialogs": { - "name": "تعطيل مربعات حوار التأكيد", - "description": "يؤكد تلقائياً الإجراءات المحددة" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0645\u0631\u0628\u0639\u0627\u062a \u062d\u0648\u0627\u0631 \u0627\u0644\u062a\u0623\u0643\u064a\u062f", + "description": "\u064a\u0624\u0643\u062f \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0627\u0644\u0625\u062c\u0631\u0627\u0621\u0627\u062a \u0627\u0644\u0645\u062d\u062f\u062f\u0629" }, "auto_updater": { - "name": "المحدث التلقائي", - "description": "يتحقق تلقائياً من وجود تحديثات جديدة" + "name": "\u0627\u0644\u0645\u062d\u062f\u062b \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u064a\u062a\u062d\u0642\u0642 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0645\u0646 \u0648\u062c\u0648\u062f \u062a\u062d\u062f\u064a\u062b\u0627\u062a \u062c\u062f\u064a\u062f\u0629" }, "disable_metrics": { - "name": "تعطيل المقاييس", - "description": "يحظر إرسال بيانات تحليلية محددة إلى Snapchat" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0627\u0644\u0645\u0642\u0627\u064a\u064a\u0633", + "description": "\u064a\u062d\u0638\u0631 \u0625\u0631\u0633\u0627\u0644 \u0628\u064a\u0627\u0646\u0627\u062a \u062a\u062d\u0644\u064a\u0644\u064a\u0629 \u0645\u062d\u062f\u062f\u0629 \u0625\u0644\u0649 Snapchat" }, "disable_story_sections": { - "name": "تعطيل أقسام القصة", - "description": "يزيل الأقسام من صفحة القصص\nقد يتطلب تحديثاً ليعمل بشكل صحيح" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0623\u0642\u0633\u0627\u0645 \u0627\u0644\u0642\u0635\u0629", + "description": "\u064a\u0632\u064a\u0644 \u0627\u0644\u0623\u0642\u0633\u0627\u0645 \u0645\u0646 \u0635\u0641\u062d\u0629 \u0627\u0644\u0642\u0635\u0635\n\u0642\u062f \u064a\u062a\u0637\u0644\u0628 \u062a\u062d\u062f\u064a\u062b\u0627\u064b \u0644\u064a\u0639\u0645\u0644 \u0628\u0634\u0643\u0644 \u0635\u062d\u064a\u062d" }, "block_ads": { - "name": "حظر الإعلانات", - "description": "يمنع عرض الإعلانات" + "name": "\u062d\u0638\u0631 \u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a", + "description": "\u064a\u0645\u0646\u0639 \u0639\u0631\u0636 \u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a" }, "disable_custom_tabs": { - "name": "تعطيل علامات التبويب المخصصة", - "description": "يفتح الروابط في التطبيقات المدعومة بدلاً من متصفح الويب" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0639\u0644\u0627\u0645\u0627\u062a \u0627\u0644\u062a\u0628\u0648\u064a\u0628 \u0627\u0644\u0645\u062e\u0635\u0635\u0629", + "description": "\u064a\u0641\u062a\u062d \u0627\u0644\u0631\u0648\u0627\u0628\u0637 \u0641\u064a \u0627\u0644\u062a\u0637\u0628\u064a\u0642\u0627\u062a \u0627\u0644\u0645\u062f\u0639\u0648\u0645\u0629 \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0645\u062a\u0635\u0641\u062d \u0627\u0644\u0648\u064a\u0628" }, "disable_permission_requests": { - "name": "تعطيل طلبات الأذونات", - "description": "يمنع Snapchat من طلب أذونات محددة" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0637\u0644\u0628\u0627\u062a \u0627\u0644\u0623\u0630\u0648\u0646\u0627\u062a", + "description": "\u064a\u0645\u0646\u0639 Snapchat \u0645\u0646 \u0637\u0644\u0628 \u0623\u0630\u0648\u0646\u0627\u062a \u0645\u062d\u062f\u062f\u0629" }, "disable_memories_snap_feed": { - "name": "تعطيل موجز الذكريات", - "description": "يمنع Snapchat من عرض الذكريات الحديثة عند التمرير لأعلى في الكاميرا" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0645\u0648\u062c\u0632 \u0627\u0644\u0630\u0643\u0631\u064a\u0627\u062a", + "description": "\u064a\u0645\u0646\u0639 Snapchat \u0645\u0646 \u0639\u0631\u0636 \u0627\u0644\u0630\u0643\u0631\u064a\u0627\u062a \u0627\u0644\u062d\u062f\u064a\u062b\u0629 \u0639\u0646\u062f \u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0644\u0623\u0639\u0644\u0649 \u0641\u064a \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627" }, "spotlight_comments_username": { - "name": "اسم مستخدم تعليقات Spotlight", - "description": "يعرض اسم المستخدم للمؤلف في تعليقات Spotlight" + "name": "\u0627\u0633\u0645 \u0645\u0633\u062a\u062e\u062f\u0645 \u062a\u0639\u0644\u064a\u0642\u0627\u062a Spotlight", + "description": "\u064a\u0639\u0631\u0636 \u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0644\u0644\u0645\u0624\u0644\u0641 \u0641\u064a \u062a\u0639\u0644\u064a\u0642\u0627\u062a Spotlight" }, "spotlight_comments_username_icon": { - "name": "أيقونة اسم مستخدم تعليقات Spotlight", - "description": "اختر الأيقونة التي يتم عرضها بجوار أسماء المستخدمين في تعليقات Spotlight" + "name": "\u0623\u064a\u0642\u0648\u0646\u0629 \u0627\u0633\u0645 \u0645\u0633\u062a\u062e\u062f\u0645 \u062a\u0639\u0644\u064a\u0642\u0627\u062a Spotlight", + "description": "\u0627\u062e\u062a\u0631 \u0627\u0644\u0623\u064a\u0642\u0648\u0646\u0629 \u0627\u0644\u062a\u064a \u064a\u062a\u0645 \u0639\u0631\u0636\u0647\u0627 \u0628\u062c\u0648\u0627\u0631 \u0623\u0633\u0645\u0627\u0621 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645\u064a\u0646 \u0641\u064a \u062a\u0639\u0644\u064a\u0642\u0627\u062a Spotlight" }, "spotlight_creator_info": { - "name": "معلومات منشئ Spotlight", - "description": "عرض زر معلومات على snaps Spotlight/Discover لعرض اسم العرض واسم المستخدم ومعرف المستخدم للمنشئ" + "name": "\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0645\u0646\u0634\u0626 Spotlight", + "description": "\u0639\u0631\u0636 \u0632\u0631 \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0639\u0644\u0649 snaps Spotlight/Discover \u0644\u0639\u0631\u0636 \u0627\u0633\u0645 \u0627\u0644\u0639\u0631\u0636 \u0648\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0648\u0645\u0639\u0631\u0641 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0644\u0644\u0645\u0646\u0634\u0626" }, "bypass_video_length_restriction": { - "name": "تجاوز قيود طول الفيديو", - "description": "فردي: يرسل فيديو واحد\nمقسم: تقسيم الفيديوهات بعد التحرير" + "name": "\u062a\u062c\u0627\u0648\u0632 \u0642\u064a\u0648\u062f \u0637\u0648\u0644 \u0627\u0644\u0641\u064a\u062f\u064a\u0648", + "description": "\u0641\u0631\u062f\u064a: \u064a\u0631\u0633\u0644 \u0641\u064a\u062f\u064a\u0648 \u0648\u0627\u062d\u062f\n\u0645\u0642\u0633\u0645: \u062a\u0642\u0633\u064a\u0645 \u0627\u0644\u0641\u064a\u062f\u064a\u0648\u0647\u0627\u062a \u0628\u0639\u062f \u0627\u0644\u062a\u062d\u0631\u064a\u0631" }, "default_video_playback_rate": { - "name": "معدل تشغيل الفيديو الافتراضي", - "description": "يحدد السرعة الافتراضية لتشغيل الفيديوهات\nيجب أن تكون القيمة بين 0.1 و 4.0" + "name": "\u0645\u0639\u062f\u0644 \u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0641\u064a\u062f\u064a\u0648 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a", + "description": "\u064a\u062d\u062f\u062f \u0627\u0644\u0633\u0631\u0639\u0629 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a\u0629 \u0644\u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0641\u064a\u062f\u064a\u0648\u0647\u0627\u062a\n\u064a\u062c\u0628 \u0623\u0646 \u062a\u0643\u0648\u0646 \u0627\u0644\u0642\u064a\u0645\u0629 \u0628\u064a\u0646 0.1 \u0648 4.0" }, "video_playback_rate_slider": { - "name": "شريط تمرير معدل تشغيل الفيديو", - "description": "يضيف شريط تمرير في قائمة سياق أوبرا لتغيير معدل تشغيل الفيديو\nملاحظة: تنطبق التغييرات فقط على الفيديوهات اللاحقة" + "name": "\u0634\u0631\u064a\u0637 \u062a\u0645\u0631\u064a\u0631 \u0645\u0639\u062f\u0644 \u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0641\u064a\u062f\u064a\u0648", + "description": "\u064a\u0636\u064a\u0641 \u0634\u0631\u064a\u0637 \u062a\u0645\u0631\u064a\u0631 \u0641\u064a \u0642\u0627\u0626\u0645\u0629 \u0633\u064a\u0627\u0642 \u0623\u0648\u0628\u0631\u0627 \u0644\u062a\u063a\u064a\u064a\u0631 \u0645\u0639\u062f\u0644 \u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0641\u064a\u062f\u064a\u0648\n\u0645\u0644\u0627\u062d\u0638\u0629: \u062a\u0646\u0637\u0628\u0642 \u0627\u0644\u062a\u063a\u064a\u064a\u0631\u0627\u062a \u0641\u0642\u0637 \u0639\u0644\u0649 \u0627\u0644\u0641\u064a\u062f\u064a\u0648\u0647\u0627\u062a \u0627\u0644\u0644\u0627\u062d\u0642\u0629" }, "disable_google_play_dialogs": { - "name": "تعطيل مربعات حوار خدمات Google Play", - "description": "منع عرض مربعات حوار توفر خدمات Google Play" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0645\u0631\u0628\u0639\u0627\u062a \u062d\u0648\u0627\u0631 \u062e\u062f\u0645\u0627\u062a Google Play", + "description": "\u0645\u0646\u0639 \u0639\u0631\u0636 \u0645\u0631\u0628\u0639\u0627\u062a \u062d\u0648\u0627\u0631 \u062a\u0648\u0641\u0631 \u062e\u062f\u0645\u0627\u062a Google Play" }, "default_volume_controls": { - "name": "عناصر التحكم الافتراضية في الصوت", - "description": "يفرض على Snapchat استخدام عناصر التحكم في صوت النظام" + "name": "\u0639\u0646\u0627\u0635\u0631 \u0627\u0644\u062a\u062d\u0643\u0645 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a\u0629 \u0641\u064a \u0627\u0644\u0635\u0648\u062a", + "description": "\u064a\u0641\u0631\u0636 \u0639\u0644\u0649 Snapchat \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0639\u0646\u0627\u0635\u0631 \u0627\u0644\u062a\u062d\u0643\u0645 \u0641\u064a \u0635\u0648\u062a \u0627\u0644\u0646\u0638\u0627\u0645" }, "disable_telecom_framework": { - "name": "تعطيل إطار عمل الاتصالات", - "description": "يمنع Snapchat من استخدام إطار عمل Android Telecom\nهذا يسمح لك بالاستماع إلى الموسيقى أثناء إجراء مكالمة" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0625\u0637\u0627\u0631 \u0639\u0645\u0644 \u0627\u0644\u0627\u062a\u0635\u0627\u0644\u0627\u062a", + "description": "\u064a\u0645\u0646\u0639 Snapchat \u0645\u0646 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0625\u0637\u0627\u0631 \u0639\u0645\u0644 Android Telecom\n\u0647\u0630\u0627 \u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u0627\u0644\u0627\u0633\u062a\u0645\u0627\u0639 \u0625\u0644\u0649 \u0627\u0644\u0645\u0648\u0633\u064a\u0642\u0649 \u0623\u062b\u0646\u0627\u0621 \u0625\u062c\u0631\u0627\u0621 \u0645\u0643\u0627\u0644\u0645\u0629" }, "hide_active_music": { - "name": "إخفاء الموسيقى النشطة", - "description": "يمنع Snapchat من معرفة أنك تستمع إلى الموسيقى\nسيسمح لك ذلك بأخذ snaps باستخدام أزرار التحكم في الصوت أثناء الاستماع إلى الموسيقى" + "name": "\u0625\u062e\u0641\u0627\u0621 \u0627\u0644\u0645\u0648\u0633\u064a\u0642\u0649 \u0627\u0644\u0646\u0634\u0637\u0629", + "description": "\u064a\u0645\u0646\u0639 Snapchat \u0645\u0646 \u0645\u0639\u0631\u0641\u0629 \u0623\u0646\u0643 \u062a\u0633\u062a\u0645\u0639 \u0625\u0644\u0649 \u0627\u0644\u0645\u0648\u0633\u064a\u0642\u0649\n\u0633\u064a\u0633\u0645\u062d \u0644\u0643 \u0630\u0644\u0643 \u0628\u0623\u062e\u0630 snaps \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0623\u0632\u0631\u0627\u0631 \u0627\u0644\u062a\u062d\u0643\u0645 \u0641\u064a \u0627\u0644\u0635\u0648\u062a \u0623\u062b\u0646\u0627\u0621 \u0627\u0644\u0627\u0633\u062a\u0645\u0627\u0639 \u0625\u0644\u0649 \u0627\u0644\u0645\u0648\u0633\u064a\u0642\u0649" }, "disable_snap_splitting": { - "name": "تعطيل تقسيم Snap", - "description": "يمنع تقسيم الـ Snaps إلى أجزاء متعددة\nالصور التي ترسلها ستتحول إلى فيديوهات" + "name": "\u062a\u0639\u0637\u064a\u0644 \u062a\u0642\u0633\u064a\u0645 Snap", + "description": "\u064a\u0645\u0646\u0639 \u062a\u0642\u0633\u064a\u0645 \u0627\u0644\u0640 Snaps \u0625\u0644\u0649 \u0623\u062c\u0632\u0627\u0621 \u0645\u062a\u0639\u062f\u062f\u0629\n\u0627\u0644\u0635\u0648\u0631 \u0627\u0644\u062a\u064a \u062a\u0631\u0633\u0644\u0647\u0627 \u0633\u062a\u062a\u062d\u0648\u0644 \u0625\u0644\u0649 \u0641\u064a\u062f\u064a\u0648\u0647\u0627\u062a" } } }, "rules": { - "name": "القواعد", - "description": "تكوين قواعد الأتمتة", + "name": "\u0627\u0644\u0642\u0648\u0627\u0639\u062f", + "description": "\u062a\u0643\u0648\u064a\u0646 \u0642\u0648\u0627\u0639\u062f \u0627\u0644\u0623\u062a\u0645\u062a\u0629", "properties": { "auto_read": { - "name": "قراءة تلقائية" + "name": "\u0642\u0631\u0627\u0621\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0629" }, "hide_typing_indicator": { - "name": "إخفاء مؤشر الكتابة" + "name": "\u0625\u062e\u0641\u0627\u0621 \u0645\u0624\u0634\u0631 \u0627\u0644\u0643\u062a\u0627\u0628\u0629" }, "auto_reply": { - "name": "الرد التلقائي" + "name": "\u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a" }, "auto_delete_sent_messages": { - "name": "حذف الرسائل المرسلة تلقائياً" + "name": "\u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0631\u0633\u0644\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b" }, "auto_download": { - "name": "تنزيل تلقائي" + "name": "\u062a\u0646\u0632\u064a\u0644 \u062a\u0644\u0642\u0627\u0626\u064a" }, "stealth": { - "name": "وضع التخفي" + "name": "\u0648\u0636\u0639 \u0627\u0644\u062a\u062e\u0641\u064a" }, "auto_save": { - "name": "حفظ تلقائي" + "name": "\u062d\u0641\u0638 \u062a\u0644\u0642\u0627\u0626\u064a" }, "message_logger": { - "name": "مسجل الرسائل" + "name": "\u0645\u0633\u062c\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644" }, "unsaveable_messages": { - "name": "رسائل غير قابلة للحفظ" + "name": "\u0631\u0633\u0627\u0626\u0644 \u063a\u064a\u0631 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638" } } }, "camera": { - "name": "الكاميرا", - "description": "ضبط الإعدادات الصحيحة للحصول على snap مثالي", + "name": "\u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627", + "description": "\u0636\u0628\u0637 \u0627\u0644\u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u0635\u062d\u064a\u062d\u0629 \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 snap \u0645\u062b\u0627\u0644\u064a", "properties": { "disable_cameras": { - "name": "تعطيل الكاميرات", - "description": "يمنع Snapchat من استخدام الكاميرات المحددة" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627\u062a", + "description": "\u064a\u0645\u0646\u0639 Snapchat \u0645\u0646 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627\u062a \u0627\u0644\u0645\u062d\u062f\u062f\u0629" }, "black_photos": { - "name": "صور سوداء", - "description": "يستبدل الصور الملتقطة بخلفية سوداء\nالفيديوهات لا تتأثر" + "name": "\u0635\u0648\u0631 \u0633\u0648\u062f\u0627\u0621", + "description": "\u064a\u0633\u062a\u0628\u062f\u0644 \u0627\u0644\u0635\u0648\u0631 \u0627\u0644\u0645\u0644\u062a\u0642\u0637\u0629 \u0628\u062e\u0644\u0641\u064a\u0629 \u0633\u0648\u062f\u0627\u0621\n\u0627\u0644\u0641\u064a\u062f\u064a\u0648\u0647\u0627\u062a \u0644\u0627 \u062a\u062a\u0623\u062b\u0631" }, "immersive_camera_preview": { - "name": "معاينة غامرة", - "description": "يمنع Snapchat من قص معاينة الكاميرا\nقد يتسبب هذا في وميض الكاميرا على بعض الأجهزة" + "name": "\u0645\u0639\u0627\u064a\u0646\u0629 \u063a\u0627\u0645\u0631\u0629", + "description": "\u064a\u0645\u0646\u0639 Snapchat \u0645\u0646 \u0642\u0635 \u0645\u0639\u0627\u064a\u0646\u0629 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627\n\u0642\u062f \u064a\u062a\u0633\u0628\u0628 \u0647\u0630\u0627 \u0641\u064a \u0648\u0645\u064a\u0636 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0639\u0644\u0649 \u0628\u0639\u0636 \u0627\u0644\u0623\u062c\u0647\u0632\u0629" }, "override_front_resolution": { - "name": "تجاوز دقة الكاميرا الأمامية", - "description": "يتجاوز دقة الكاميرا للكاميرا الأمامية" + "name": "\u062a\u062c\u0627\u0648\u0632 \u062f\u0642\u0629 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u0623\u0645\u0627\u0645\u064a\u0629", + "description": "\u064a\u062a\u062c\u0627\u0648\u0632 \u062f\u0642\u0629 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0644\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u0623\u0645\u0627\u0645\u064a\u0629" }, "override_back_resolution": { - "name": "تجاوز دقة الكاميرا الخلفية", - "description": "يتجاوز دقة الكاميرا للكاميرا الخلفية" + "name": "\u062a\u062c\u0627\u0648\u0632 \u062f\u0642\u0629 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u062e\u0644\u0641\u064a\u0629", + "description": "\u064a\u062a\u062c\u0627\u0648\u0632 \u062f\u0642\u0629 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0644\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u062e\u0644\u0641\u064a\u0629" }, "custom_resolution": { - "name": "دقة مخصصة", - "description": "يحدد دقة كاميرا مخصصة، العرض x الارتفاع (مثلاً 1920x1080).\nيجب أن تكون الدقة المخصصة مدعومة بواسطة جهازك" + "name": "\u062f\u0642\u0629 \u0645\u062e\u0635\u0635\u0629", + "description": "\u064a\u062d\u062f\u062f \u062f\u0642\u0629 \u0643\u0627\u0645\u064a\u0631\u0627 \u0645\u062e\u0635\u0635\u0629\u060c \u0627\u0644\u0639\u0631\u0636 x \u0627\u0644\u0627\u0631\u062a\u0641\u0627\u0639 (\u0645\u062b\u0644\u0627\u064b 1920x1080).\n\u064a\u062c\u0628 \u0623\u0646 \u062a\u0643\u0648\u0646 \u0627\u0644\u062f\u0642\u0629 \u0627\u0644\u0645\u062e\u0635\u0635\u0629 \u0645\u062f\u0639\u0648\u0645\u0629 \u0628\u0648\u0627\u0633\u0637\u0629 \u062c\u0647\u0627\u0632\u0643" }, "front_custom_frame_rate": { - "name": "معدل إطارات مخصص للكاميرا الأمامية", - "description": "يتجاوز معدل إطارات الكاميرا الأمامية" + "name": "\u0645\u0639\u062f\u0644 \u0625\u0637\u0627\u0631\u0627\u062a \u0645\u062e\u0635\u0635 \u0644\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u0623\u0645\u0627\u0645\u064a\u0629", + "description": "\u064a\u062a\u062c\u0627\u0648\u0632 \u0645\u0639\u062f\u0644 \u0625\u0637\u0627\u0631\u0627\u062a \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u0623\u0645\u0627\u0645\u064a\u0629" }, "back_custom_frame_rate": { - "name": "معدل إطارات مخصص للكاميرا الخلفية", - "description": "يتجاوز معدل إطارات الكاميرا الخلفية" + "name": "\u0645\u0639\u062f\u0644 \u0625\u0637\u0627\u0631\u0627\u062a \u0645\u062e\u0635\u0635 \u0644\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u062e\u0644\u0641\u064a\u0629", + "description": "\u064a\u062a\u062c\u0627\u0648\u0632 \u0645\u0639\u062f\u0644 \u0625\u0637\u0627\u0631\u0627\u062a \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u062e\u0644\u0641\u064a\u0629" }, "force_camera_source_encoding": { - "name": "فرض تشفير مصدر الكاميرا", - "description": "يفرض تشفير مصدر الكاميرا" + "name": "\u0641\u0631\u0636 \u062a\u0634\u0641\u064a\u0631 \u0645\u0635\u062f\u0631 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627", + "description": "\u064a\u0641\u0631\u0636 \u062a\u0634\u0641\u064a\u0631 \u0645\u0635\u062f\u0631 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627" }, "startup_default_camera": { - "name": "كاميرا البدء الافتراضية", - "description": "يحدد الكاميرا الافتراضية عند فتح Snapchat" + "name": "\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u0628\u062f\u0621 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a\u0629", + "description": "\u064a\u062d\u062f\u062f \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a\u0629 \u0639\u0646\u062f \u0641\u062a\u062d Snapchat" }, "hevc_recording": { - "name": "تسجيل HEVC", - "description": "يستخدم ترميز HEVC (H.265) لتسجيل الفيديو" + "name": "\u062a\u0633\u062c\u064a\u0644 HEVC", + "description": "\u064a\u0633\u062a\u062e\u062f\u0645 \u062a\u0631\u0645\u064a\u0632 HEVC (H.265) \u0644\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u0641\u064a\u062f\u064a\u0648" }, "video_record_timer": { - "name": "مؤقت تسجيل الفيديو", - "description": "يعرض تراكب مؤقت التسجيل عند تسجيل الفيديو" + "name": "\u0645\u0624\u0642\u062a \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u0641\u064a\u062f\u064a\u0648", + "description": "\u064a\u0639\u0631\u0636 \u062a\u0631\u0627\u0643\u0628 \u0645\u0624\u0642\u062a \u0627\u0644\u062a\u0633\u062c\u064a\u0644 \u0639\u0646\u062f \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u0641\u064a\u062f\u064a\u0648" } } }, "streaks_reminder": { - "name": "تذكير الستريك", - "description": "ينبهك بشكل دوري حول الستريك (Streaks)", + "name": "\u062a\u0630\u0643\u064a\u0631 \u0627\u0644\u0633\u062a\u0631\u064a\u0643", + "description": "\u064a\u0646\u0628\u0647\u0643 \u0628\u0634\u0643\u0644 \u062f\u0648\u0631\u064a \u062d\u0648\u0644 \u0627\u0644\u0633\u062a\u0631\u064a\u0643 (Streaks)", "properties": { "interval": { - "name": "الفاصل الزمني", - "description": "الفاصل الزمني بين كل تذكير (بالساعات)" + "name": "\u0627\u0644\u0641\u0627\u0635\u0644 \u0627\u0644\u0632\u0645\u0646\u064a", + "description": "\u0627\u0644\u0641\u0627\u0635\u0644 \u0627\u0644\u0632\u0645\u0646\u064a \u0628\u064a\u0646 \u0643\u0644 \u062a\u0630\u0643\u064a\u0631 (\u0628\u0627\u0644\u0633\u0627\u0639\u0627\u062a)" }, "remaining_hours": { - "name": "الوقت المتبقي", - "description": "الوقت المتبقي قبل عرض الإشعار (بالساعات)" + "name": "\u0627\u0644\u0648\u0642\u062a \u0627\u0644\u0645\u062a\u0628\u0642\u064a", + "description": "\u0627\u0644\u0648\u0642\u062a \u0627\u0644\u0645\u062a\u0628\u0642\u064a \u0642\u0628\u0644 \u0639\u0631\u0636 \u0627\u0644\u0625\u0634\u0639\u0627\u0631 (\u0628\u0627\u0644\u0633\u0627\u0639\u0627\u062a)" }, "group_notifications": { - "name": "إشعارات المجموعة", - "description": "تجميع الإشعارات في واحد" + "name": "\u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0629", + "description": "\u062a\u062c\u0645\u064a\u0639 \u0627\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0641\u064a \u0648\u0627\u062d\u062f" } } }, "experimental": { - "name": "تجريبي", - "description": "ميزات تجريبية", + "name": "\u062a\u062c\u0631\u064a\u0628\u064a", + "description": "\u0645\u064a\u0632\u0627\u062a \u062a\u062c\u0631\u064a\u0628\u064a\u0629", "properties": { "native_hooks": { "name": "Native Hooks", - "description": "ميزات غير آمنة تتصل بكود Snapchat الأصلي", + "description": "\u0645\u064a\u0632\u0627\u062a \u063a\u064a\u0631 \u0622\u0645\u0646\u0629 \u062a\u062a\u0635\u0644 \u0628\u0643\u0648\u062f Snapchat \u0627\u0644\u0623\u0635\u0644\u064a", "properties": { "composer_hooks": { "name": "Composer Hooks", - "description": "يحقن كود في إطار عمل واجهة المستخدم Composer عبر المنصات", + "description": "\u064a\u062d\u0642\u0646 \u0643\u0648\u062f \u0641\u064a \u0625\u0637\u0627\u0631 \u0639\u0645\u0644 \u0648\u0627\u062c\u0647\u0629 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 Composer \u0639\u0628\u0631 \u0627\u0644\u0645\u0646\u0635\u0627\u062a", "properties": { "show_first_created_username": { - "name": "عرض اسم المستخدم الذي تم إنشاؤه أولاً", - "description": "يعرض اسم المستخدم الذي تم إنشاؤه أولاً بجوار اسم المستخدم الحالي في صفحة الملف الشخصي" + "name": "\u0639\u0631\u0636 \u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u0630\u064a \u062a\u0645 \u0625\u0646\u0634\u0627\u0624\u0647 \u0623\u0648\u0644\u0627\u064b", + "description": "\u064a\u0639\u0631\u0636 \u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u0630\u064a \u062a\u0645 \u0625\u0646\u0634\u0627\u0624\u0647 \u0623\u0648\u0644\u0627\u064b \u0628\u062c\u0648\u0627\u0631 \u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u062d\u0627\u0644\u064a \u0641\u064a \u0635\u0641\u062d\u0629 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a" }, "bypass_camera_roll_limit": { - "name": "تجاوز حد ألبوم الكاميرا", - "description": "يزيد الحد الأقصى لكمية الوسائط التي يمكنك إرسالها من ألبوم الكاميرا" + "name": "\u062a\u062c\u0627\u0648\u0632 \u062d\u062f \u0623\u0644\u0628\u0648\u0645 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627", + "description": "\u064a\u0632\u064a\u062f \u0627\u0644\u062d\u062f \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0643\u0645\u064a\u0629 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u062a\u064a \u064a\u0645\u0643\u0646\u0643 \u0625\u0631\u0633\u0627\u0644\u0647\u0627 \u0645\u0646 \u0623\u0644\u0628\u0648\u0645 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627" }, "custom_self_destruct_snap_delay": { - "name": "تأخير مخصص للتدمير الذاتي للـ Snap", - "description": "يعطي المزيد من الخيارات لمؤقت التدمير الذاتي عند إرسال Snap" + "name": "\u062a\u0623\u062e\u064a\u0631 \u0645\u062e\u0635\u0635 \u0644\u0644\u062a\u062f\u0645\u064a\u0631 \u0627\u0644\u0630\u0627\u062a\u064a \u0644\u0644\u0640 Snap", + "description": "\u064a\u0639\u0637\u064a \u0627\u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062e\u064a\u0627\u0631\u0627\u062a \u0644\u0645\u0624\u0642\u062a \u0627\u0644\u062a\u062f\u0645\u064a\u0631 \u0627\u0644\u0630\u0627\u062a\u064a \u0639\u0646\u062f \u0625\u0631\u0633\u0627\u0644 Snap" }, "composer_console": { - "name": "وحدة تحكم Composer", - "description": "تسمح لك بتنفيذ كود JavaScript في Composer (arm64 فقط)" + "name": "\u0648\u062d\u062f\u0629 \u062a\u062d\u0643\u0645 Composer", + "description": "\u062a\u0633\u0645\u062d \u0644\u0643 \u0628\u062a\u0646\u0641\u064a\u0630 \u0643\u0648\u062f JavaScript \u0641\u064a Composer (arm64 \u0641\u0642\u0637)" }, "composer_logs": { - "name": "سجلات Composer", - "description": "تعيد توجيه سجلات وحدة التحكم الخاصة بـ Composer إلى PurrfectSnap" + "name": "\u0633\u062c\u0644\u0627\u062a Composer", + "description": "\u062a\u0639\u064a\u062f \u062a\u0648\u062c\u064a\u0647 \u0633\u062c\u0644\u0627\u062a \u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u062d\u0643\u0645 \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0640 Composer \u0625\u0644\u0649 PurrfectSnap" } } }, "disable_bitmoji": { - "name": "تعطيل Bitmoji", - "description": "يعطل Bitmoji للملف الشخصي للأصدقاء" + "name": "\u062a\u0639\u0637\u064a\u0644 Bitmoji", + "description": "\u064a\u0639\u0637\u0644 Bitmoji \u0644\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a \u0644\u0644\u0623\u0635\u062f\u0642\u0627\u0621" }, "custom_emoji_font": { - "name": "خط إيموجي مخصص", - "description": "يسمح لك باستخدام خط إيموجي مخصص. يعمل فقط مع خطوط .ttf" + "name": "\u062e\u0637 \u0625\u064a\u0645\u0648\u062c\u064a \u0645\u062e\u0635\u0635", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u062e\u0637 \u0625\u064a\u0645\u0648\u062c\u064a \u0645\u062e\u0635\u0635. \u064a\u0639\u0645\u0644 \u0641\u0642\u0637 \u0645\u0639 \u062e\u0637\u0648\u0637 .ttf" }, "custom_shared_library": { - "name": "مكتبة مشتركة مخصصة", - "description": "تحمل مكتبة مشتركة مخصصة في Snapchat. هذه الميزة لأغراض الاختبار فقط" + "name": "\u0645\u0643\u062a\u0628\u0629 \u0645\u0634\u062a\u0631\u0643\u0629 \u0645\u062e\u0635\u0635\u0629", + "description": "\u062a\u062d\u0645\u0644 \u0645\u0643\u062a\u0628\u0629 \u0645\u0634\u062a\u0631\u0643\u0629 \u0645\u062e\u0635\u0635\u0629 \u0641\u064a Snapchat. \u0647\u0630\u0647 \u0627\u0644\u0645\u064a\u0632\u0629 \u0644\u0623\u063a\u0631\u0627\u0636 \u0627\u0644\u0627\u062e\u062a\u0628\u0627\u0631 \u0641\u0642\u0637" } } }, "spoof": { - "name": "تزييف (Spoof)", - "description": "تزييف معلومات مختلفة عنك", + "name": "\u062a\u0632\u064a\u064a\u0641 (Spoof)", + "description": "\u062a\u0632\u064a\u064a\u0641 \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0645\u062e\u062a\u0644\u0641\u0629 \u0639\u0646\u0643", "properties": { "play_store_installer_package_name": { - "name": "اسم حزمة مثبت متجر Play", - "description": "يتجاوز اسم حزمة المثبت إلى com.android.vending" + "name": "\u0627\u0633\u0645 \u062d\u0632\u0645\u0629 \u0645\u062b\u0628\u062a \u0645\u062a\u062c\u0631 Play", + "description": "\u064a\u062a\u062c\u0627\u0648\u0632 \u0627\u0633\u0645 \u062d\u0632\u0645\u0629 \u0627\u0644\u0645\u062b\u0628\u062a \u0625\u0644\u0649 com.android.vending" }, "remove_vpn_transport_flag": { - "name": "إزالة علامة نقل VPN", - "description": "يمنع Snapchat من اكتشاف الـ VPN" + "name": "\u0625\u0632\u0627\u0644\u0629 \u0639\u0644\u0627\u0645\u0629 \u0646\u0642\u0644 VPN", + "description": "\u064a\u0645\u0646\u0639 Snapchat \u0645\u0646 \u0627\u0643\u062a\u0634\u0627\u0641 \u0627\u0644\u0640 VPN" }, "remove_mock_location_flag": { - "name": "إزالة علامة الموقع الوهمي", - "description": "يمنع Snapchat من اكتشاف الموقع الوهمي (Mock location)" + "name": "\u0625\u0632\u0627\u0644\u0629 \u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u0645\u0648\u0642\u0639 \u0627\u0644\u0648\u0647\u0645\u064a", + "description": "\u064a\u0645\u0646\u0639 Snapchat \u0645\u0646 \u0627\u0643\u062a\u0634\u0627\u0641 \u0627\u0644\u0645\u0648\u0642\u0639 \u0627\u0644\u0648\u0647\u0645\u064a (Mock location)" }, "force_wifi_transport_flag": { - "name": "فرض علامة نقل Wi-Fi", - "description": "فرض نقل الشبكة للإبلاغ عن Wi-Fi بدلاً من بيانات الهاتف المحمول" + "name": "\u0641\u0631\u0636 \u0639\u0644\u0627\u0645\u0629 \u0646\u0642\u0644 Wi-Fi", + "description": "\u0641\u0631\u0636 \u0646\u0642\u0644 \u0627\u0644\u0634\u0628\u0643\u0629 \u0644\u0644\u0625\u0628\u0644\u0627\u063a \u0639\u0646 Wi-Fi \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0647\u0627\u062a\u0641 \u0627\u0644\u0645\u062d\u0645\u0648\u0644" }, "spoof_device_id": { - "name": "تزييف معرف الجهاز", - "description": "تجاوز معرف Android المرسل إلى Snapchat", + "name": "\u062a\u0632\u064a\u064a\u0641 \u0645\u0639\u0631\u0641 \u0627\u0644\u062c\u0647\u0627\u0632", + "description": "\u062a\u062c\u0627\u0648\u0632 \u0645\u0639\u0631\u0641 Android \u0627\u0644\u0645\u0631\u0633\u0644 \u0625\u0644\u0649 Snapchat", "properties": { "spoof_android_id": { - "name": "تزييف معرف Android", - "description": "تجاوز معرف Android المرسل إلى Snapchat بقيمة مخصصة" + "name": "\u062a\u0632\u064a\u064a\u0641 \u0645\u0639\u0631\u0641 Android", + "description": "\u062a\u062c\u0627\u0648\u0632 \u0645\u0639\u0631\u0641 Android \u0627\u0644\u0645\u0631\u0633\u0644 \u0625\u0644\u0649 Snapchat \u0628\u0642\u064a\u0645\u0629 \u0645\u062e\u0635\u0635\u0629" }, "custom_android_id": { - "name": "معرف Android مخصص", - "description": "القيمة المستخدمة عند تزييف معرف Android" + "name": "\u0645\u0639\u0631\u0641 Android \u0645\u062e\u0635\u0635", + "description": "\u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645\u0629 \u0639\u0646\u062f \u062a\u0632\u064a\u064a\u0641 \u0645\u0639\u0631\u0641 Android" } } }, "spoof_device": { - "name": "تزييف الجهاز", - "description": "تقديم Snapchat وكأنه يعمل على طراز جهاز آخر" + "name": "\u062a\u0632\u064a\u064a\u0641 \u0627\u0644\u062c\u0647\u0627\u0632", + "description": "\u062a\u0642\u062f\u064a\u0645 Snapchat \u0648\u0643\u0623\u0646\u0647 \u064a\u0639\u0645\u0644 \u0639\u0644\u0649 \u0637\u0631\u0627\u0632 \u062c\u0647\u0627\u0632 \u0622\u062e\u0631" }, "device_model": { - "name": "طراز الجهاز", - "description": "اختر طراز الجهاز المراد تزييفه" + "name": "\u0637\u0631\u0627\u0632 \u0627\u0644\u062c\u0647\u0627\u0632", + "description": "\u0627\u062e\u062a\u0631 \u0637\u0631\u0627\u0632 \u0627\u0644\u062c\u0647\u0627\u0632 \u0627\u0644\u0645\u0631\u0627\u062f \u062a\u0632\u064a\u064a\u0641\u0647" } } }, "convert_message_locally": { - "name": "تحويل الرسالة محلياً", - "description": "يحول الـ snaps إلى وسائط خارجية للدردشة محلياً. يظهر هذا في قائمة سياق تنزيل الدردشة" + "name": "\u062a\u062d\u0648\u064a\u0644 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u0645\u062d\u0644\u064a\u0627\u064b", + "description": "\u064a\u062d\u0648\u0644 \u0627\u0644\u0640 snaps \u0625\u0644\u0649 \u0648\u0633\u0627\u0626\u0637 \u062e\u0627\u0631\u062c\u064a\u0629 \u0644\u0644\u062f\u0631\u062f\u0634\u0629 \u0645\u062d\u0644\u064a\u0627\u064b. \u064a\u0638\u0647\u0631 \u0647\u0630\u0627 \u0641\u064a \u0642\u0627\u0626\u0645\u0629 \u0633\u064a\u0627\u0642 \u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u062f\u0631\u062f\u0634\u0629" }, "media_file_picker": { - "name": "منتقي ملفات الوسائط", - "description": "يسمح لك باختيار أي ملف فيديو/صوت من المعرض" + "name": "\u0645\u0646\u062a\u0642\u064a \u0645\u0644\u0641\u0627\u062a \u0627\u0644\u0648\u0633\u0627\u0626\u0637", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u0627\u062e\u062a\u064a\u0627\u0631 \u0623\u064a \u0645\u0644\u0641 \u0641\u064a\u062f\u064a\u0648/\u0635\u0648\u062a \u0645\u0646 \u0627\u0644\u0645\u0639\u0631\u0636" }, "story_logger": { - "name": "مسجل القصص", - "description": "يوفر تاريخاً لقصص الأصدقاء" + "name": "\u0645\u0633\u062c\u0644 \u0627\u0644\u0642\u0635\u0635", + "description": "\u064a\u0648\u0641\u0631 \u062a\u0627\u0631\u064a\u062e\u0627\u064b \u0644\u0642\u0635\u0635 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621" }, "account_switcher": { - "name": "مبدل الحسابات", - "description": "يسمح لك بالتبديل بين الحسابات دون تسجيل الخروج\nاضغط مطولاً على أيقونة البحث بجوار ملف Bitmoji الخاص بك لفتح القائمة\nملاحظة: هذه الميزة تجريبية ومن المرجح أن تتغير في المستقبل", + "name": "\u0645\u0628\u062f\u0644 \u0627\u0644\u062d\u0633\u0627\u0628\u0627\u062a", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u0627\u0644\u062a\u0628\u062f\u064a\u0644 \u0628\u064a\u0646 \u0627\u0644\u062d\u0633\u0627\u0628\u0627\u062a \u062f\u0648\u0646 \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062e\u0631\u0648\u062c\n\u0627\u0636\u063a\u0637 \u0645\u0637\u0648\u0644\u0627\u064b \u0639\u0644\u0649 \u0623\u064a\u0642\u0648\u0646\u0629 \u0627\u0644\u0628\u062d\u062b \u0628\u062c\u0648\u0627\u0631 \u0645\u0644\u0641 Bitmoji \u0627\u0644\u062e\u0627\u0635 \u0628\u0643 \u0644\u0641\u062a\u062d \u0627\u0644\u0642\u0627\u0626\u0645\u0629\n\u0645\u0644\u0627\u062d\u0638\u0629: \u0647\u0630\u0647 \u0627\u0644\u0645\u064a\u0632\u0629 \u062a\u062c\u0631\u064a\u0628\u064a\u0629 \u0648\u0645\u0646 \u0627\u0644\u0645\u0631\u062c\u062d \u0623\u0646 \u062a\u062a\u063a\u064a\u0631 \u0641\u064a \u0627\u0644\u0645\u0633\u062a\u0642\u0628\u0644", "properties": { "auto_backup_current_account": { - "name": "نسخ احتياطي تلقائي للحساب الحالي", - "description": "يقوم بالنسخ الاحتياطي للحساب الحالي تلقائياً عند تسجيل الخروج أو تبديل الحسابات" + "name": "\u0646\u0633\u062e \u0627\u062d\u062a\u064a\u0627\u0637\u064a \u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0644\u062d\u0633\u0627\u0628 \u0627\u0644\u062d\u0627\u0644\u064a", + "description": "\u064a\u0642\u0648\u0645 \u0628\u0627\u0644\u0646\u0633\u062e \u0627\u0644\u0627\u062d\u062a\u064a\u0627\u0637\u064a \u0644\u0644\u062d\u0633\u0627\u0628 \u0627\u0644\u062d\u0627\u0644\u064a \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0639\u0646\u062f \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062e\u0631\u0648\u062c \u0623\u0648 \u062a\u0628\u062f\u064a\u0644 \u0627\u0644\u062d\u0633\u0627\u0628\u0627\u062a" } } }, "better_transcript": { - "name": "نسخ نصي أفضل", - "description": "يحسن النسخ النصي للملاحظات الصوتية", + "name": "\u0646\u0633\u062e \u0646\u0635\u064a \u0623\u0641\u0636\u0644", + "description": "\u064a\u062d\u0633\u0646 \u0627\u0644\u0646\u0633\u062e \u0627\u0644\u0646\u0635\u064a \u0644\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629", "properties": { "force_transcription": { - "name": "فرض نسخ الملاحظات الصوتية", - "description": "يسمح بنسخ جميع الملاحظات الصوتية" + "name": "\u0641\u0631\u0636 \u0646\u0633\u062e \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629", + "description": "\u064a\u0633\u0645\u062d \u0628\u0646\u0633\u062e \u062c\u0645\u064a\u0639 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629" }, "preferred_transcription_lang": { - "name": "لغة النسخ المفضلة", - "description": "اللغة المفضلة للنسخ النصي للملاحظات الصوتية (مثل EN، ES، FR)" + "name": "\u0644\u063a\u0629 \u0627\u0644\u0646\u0633\u062e \u0627\u0644\u0645\u0641\u0636\u0644\u0629", + "description": "\u0627\u0644\u0644\u063a\u0629 \u0627\u0644\u0645\u0641\u0636\u0644\u0629 \u0644\u0644\u0646\u0633\u062e \u0627\u0644\u0646\u0635\u064a \u0644\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629 (\u0645\u062b\u0644 EN\u060c ES\u060c FR)" }, "notification_transcript": { - "name": "نسخ الإشعار", - "description": "ينسخ الملاحظات الصوتية في الإشعارات\nتتطلب هذه الميزة تمكين ميزة معاينة الدردشة في إشعارات أفضل" + "name": "\u0646\u0633\u062e \u0627\u0644\u0625\u0634\u0639\u0627\u0631", + "description": "\u064a\u0646\u0633\u062e \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629 \u0641\u064a \u0627\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062a\n\u062a\u062a\u0637\u0644\u0628 \u0647\u0630\u0647 \u0627\u0644\u0645\u064a\u0632\u0629 \u062a\u0645\u0643\u064a\u0646 \u0645\u064a\u0632\u0629 \u0645\u0639\u0627\u064a\u0646\u0629 \u0627\u0644\u062f\u0631\u062f\u0634\u0629 \u0641\u064a \u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0623\u0641\u0636\u0644" } } }, "voice_note_auto_play": { - "name": "تشغيل تلقائي للملاحظات الصوتية", - "description": "يشغل الملاحظة الصوتية التالية تلقائياً بعد انتهاء الحالية" + "name": "\u062a\u0634\u063a\u064a\u0644 \u062a\u0644\u0642\u0627\u0626\u064a \u0644\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629", + "description": "\u064a\u0634\u063a\u0644 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0629 \u0627\u0644\u0635\u0648\u062a\u064a\u0629 \u0627\u0644\u062a\u0627\u0644\u064a\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0628\u0639\u062f \u0627\u0646\u062a\u0647\u0627\u0621 \u0627\u0644\u062d\u0627\u0644\u064a\u0629" }, "friend_notes": { - "name": "ملاحظات الأصدقاء", - "description": "يسمح لك بإضافة ملاحظات لملفات تعريف الأصدقاء" + "name": "\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u0625\u0636\u0627\u0641\u0629 \u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0644\u0645\u0644\u0641\u0627\u062a \u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621" }, "cof_experiments": { - "name": "تجارب COF", - "description": "يمكن ميزات Snapchat غير المصدرة/التجريبية (beta)" + "name": "\u062a\u062c\u0627\u0631\u0628 COF", + "description": "\u064a\u0645\u0643\u0646 \u0645\u064a\u0632\u0627\u062a Snapchat \u063a\u064a\u0631 \u0627\u0644\u0645\u0635\u062f\u0631\u0629/\u0627\u0644\u062a\u062c\u0631\u064a\u0628\u064a\u0629 (beta)" }, "context_menu_fix": { - "name": "إصلاح قائمة السياق", - "description": "محاولة إصلاح قائمة موجز الأصدقاء حيث لا يمكن عرضها بشكل صحيح عندما يكون الجهاز غير متصل بالإنترنت" + "name": "\u0625\u0635\u0644\u0627\u062d \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0633\u064a\u0627\u0642", + "description": "\u0645\u062d\u0627\u0648\u0644\u0629 \u0625\u0635\u0644\u0627\u062d \u0642\u0627\u0626\u0645\u0629 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u062d\u064a\u062b \u0644\u0627 \u064a\u0645\u0643\u0646 \u0639\u0631\u0636\u0647\u0627 \u0628\u0634\u0643\u0644 \u0635\u062d\u064a\u062d \u0639\u0646\u062f\u0645\u0627 \u064a\u0643\u0648\u0646 \u0627\u0644\u062c\u0647\u0627\u0632 \u063a\u064a\u0631 \u0645\u062a\u0635\u0644 \u0628\u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a" }, "app_lock": { - "name": "قفل التطبيق", - "description": "يمنع الوصول إلى Snapchat بدون رمز مرور", + "name": "\u0642\u0641\u0644 \u0627\u0644\u062a\u0637\u0628\u064a\u0642", + "description": "\u064a\u0645\u0646\u0639 \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 Snapchat \u0628\u062f\u0648\u0646 \u0631\u0645\u0632 \u0645\u0631\u0648\u0631", "properties": { "lock_on_resume": { - "name": "قفل عند الاستئناف", - "description": "يقفل التطبيق عند إعادة فتحه" + "name": "\u0642\u0641\u0644 \u0639\u0646\u062f \u0627\u0644\u0627\u0633\u062a\u0626\u0646\u0627\u0641", + "description": "\u064a\u0642\u0641\u0644 \u0627\u0644\u062a\u0637\u0628\u064a\u0642 \u0639\u0646\u062f \u0625\u0639\u0627\u062f\u0629 \u0641\u062a\u062d\u0647" } } }, "infinite_story_boost": { - "name": "تعزيز القصة اللانهائي", - "description": "تجاوز تأخير حد تعزيز القصة" + "name": "\u062a\u0639\u0632\u064a\u0632 \u0627\u0644\u0642\u0635\u0629 \u0627\u0644\u0644\u0627\u0646\u0647\u0627\u0626\u064a", + "description": "\u062a\u062c\u0627\u0648\u0632 \u062a\u0623\u062e\u064a\u0631 \u062d\u062f \u062a\u0639\u0632\u064a\u0632 \u0627\u0644\u0642\u0635\u0629" }, "meo_passcode_bypass": { - "name": "تجاوز رمز مرور عيني فقط (My Eyes Only)", - "description": "تجاوز رمز مرور عيني فقط\nسيعمل هذا فقط إذا تم إدخال رمز المرور بشكل صحيح من قبل" + "name": "\u062a\u062c\u0627\u0648\u0632 \u0631\u0645\u0632 \u0645\u0631\u0648\u0631 \u0639\u064a\u0646\u064a \u0641\u0642\u0637 (My Eyes Only)", + "description": "\u062a\u062c\u0627\u0648\u0632 \u0631\u0645\u0632 \u0645\u0631\u0648\u0631 \u0639\u064a\u0646\u064a \u0641\u0642\u0637\n\u0633\u064a\u0639\u0645\u0644 \u0647\u0630\u0627 \u0641\u0642\u0637 \u0625\u0630\u0627 \u062a\u0645 \u0625\u062f\u062e\u0627\u0644 \u0631\u0645\u0632 \u0627\u0644\u0645\u0631\u0648\u0631 \u0628\u0634\u0643\u0644 \u0635\u062d\u064a\u062d \u0645\u0646 \u0642\u0628\u0644" }, "no_friend_score_delay": { - "name": "لا تأخير في نقاط الصديق", - "description": "يزيل التأخير عند عرض نقاط الأصدقاء" + "name": "\u0644\u0627 \u062a\u0623\u062e\u064a\u0631 \u0641\u064a \u0646\u0642\u0627\u0637 \u0627\u0644\u0635\u062f\u064a\u0642", + "description": "\u064a\u0632\u064a\u0644 \u0627\u0644\u062a\u0623\u062e\u064a\u0631 \u0639\u0646\u062f \u0639\u0631\u0636 \u0646\u0642\u0627\u0637 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621" }, "best_friend_pinning": { - "name": "تثبيت أفضل صديق", - "description": "يسمح لك بتثبيت صديق كأفضل صديق رقم واحد لديك. ملاحظة: أنت فقط من يمكنه رؤية أفضل صديق مثبت لديك" + "name": "\u062a\u062b\u0628\u064a\u062a \u0623\u0641\u0636\u0644 \u0635\u062f\u064a\u0642", + "description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u0628\u062a\u062b\u0628\u064a\u062a \u0635\u062f\u064a\u0642 \u0643\u0623\u0641\u0636\u0644 \u0635\u062f\u064a\u0642 \u0631\u0642\u0645 \u0648\u0627\u062d\u062f \u0644\u062f\u064a\u0643. \u0645\u0644\u0627\u062d\u0638\u0629: \u0623\u0646\u062a \u0641\u0642\u0637 \u0645\u0646 \u064a\u0645\u0643\u0646\u0647 \u0631\u0624\u064a\u0629 \u0623\u0641\u0636\u0644 \u0635\u062f\u064a\u0642 \u0645\u062b\u0628\u062a \u0644\u062f\u064a\u0643" }, "e2ee": { - "name": "التشفير من طرف لطرف", - "description": "يشفر رسائلك باستخدام AES باستخدام مفتاح سري مشترك\nتأكد من حفظ مفتاحك في مكان آمن!", + "name": "\u0627\u0644\u062a\u0634\u0641\u064a\u0631 \u0645\u0646 \u0637\u0631\u0641 \u0644\u0637\u0631\u0641", + "description": "\u064a\u0634\u0641\u0631 \u0631\u0633\u0627\u0626\u0644\u0643 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 AES \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0645\u0641\u062a\u0627\u062d \u0633\u0631\u064a \u0645\u0634\u062a\u0631\u0643\n\u062a\u0623\u0643\u062f \u0645\u0646 \u062d\u0641\u0638 \u0645\u0641\u062a\u0627\u062d\u0643 \u0641\u064a \u0645\u0643\u0627\u0646 \u0622\u0645\u0646!", "properties": { "encrypted_message_indicator": { - "name": "مؤشر الرسالة المشفرة", - "description": "يضيف إيموجي \ud83d\udd12 بجوار الرسائل المشفرة" + "name": "\u0645\u0624\u0634\u0631 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u0627\u0644\u0645\u0634\u0641\u0631\u0629", + "description": "\u064a\u0636\u064a\u0641 \u0625\u064a\u0645\u0648\u062c\u064a \ud83d\udd12 \u0628\u062c\u0648\u0627\u0631 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0634\u0641\u0631\u0629" }, "force_message_encryption": { - "name": "فرض تشفير الرسالة", - "description": "يمنع إرسال رسائل مشفرة للأشخاص الذين لم يفعلوا التشفير من طرف لطرف فقط عند تحديد محادثات متعددة" + "name": "\u0641\u0631\u0636 \u062a\u0634\u0641\u064a\u0631 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "description": "\u064a\u0645\u0646\u0639 \u0625\u0631\u0633\u0627\u0644 \u0631\u0633\u0627\u0626\u0644 \u0645\u0634\u0641\u0631\u0629 \u0644\u0644\u0623\u0634\u062e\u0627\u0635 \u0627\u0644\u0630\u064a\u0646 \u0644\u0645 \u064a\u0641\u0639\u0644\u0648\u0627 \u0627\u0644\u062a\u0634\u0641\u064a\u0631 \u0645\u0646 \u0637\u0631\u0641 \u0644\u0637\u0631\u0641 \u0641\u0642\u0637 \u0639\u0646\u062f \u062a\u062d\u062f\u064a\u062f \u0645\u062d\u0627\u062f\u062b\u0627\u062a \u0645\u062a\u0639\u062f\u062f\u0629" } } }, "add_friend_source_spoof": { - "name": "تزييف مصدر إضافة صديق", - "description": "يزيف مصدر طلب الصداقة" + "name": "\u062a\u0632\u064a\u064a\u0641 \u0645\u0635\u062f\u0631 \u0625\u0636\u0627\u0641\u0629 \u0635\u062f\u064a\u0642", + "description": "\u064a\u0632\u064a\u0641 \u0645\u0635\u062f\u0631 \u0637\u0644\u0628 \u0627\u0644\u0635\u062f\u0627\u0642\u0629" }, "hidden_snapchat_plus_features": { - "name": "ميزات Snapchat Plus المخفية", - "description": "يمكن ميزات Snapchat Plus غير المصدرة/التجريبية\nقد لا تعمل على إصدارات Snapchat القديمة" + "name": "\u0645\u064a\u0632\u0627\u062a Snapchat Plus \u0627\u0644\u0645\u062e\u0641\u064a\u0629", + "description": "\u064a\u0645\u0643\u0646 \u0645\u064a\u0632\u0627\u062a Snapchat Plus \u063a\u064a\u0631 \u0627\u0644\u0645\u0635\u062f\u0631\u0629/\u0627\u0644\u062a\u062c\u0631\u064a\u0628\u064a\u0629\n\u0642\u062f \u0644\u0627 \u062a\u0639\u0645\u0644 \u0639\u0644\u0649 \u0625\u0635\u062f\u0627\u0631\u0627\u062a Snapchat \u0627\u0644\u0642\u062f\u064a\u0645\u0629" }, "custom_streaks_expiration_format": { - "name": "تنسيق انتهاء الستريك المخصص", - "description": "يخصص تنسيق انتهاء الستريك\n\nالمتغيرات المتاحة:\n - %c: عدد الستريك\n - %e: إيموجي الساعة الرملية\n - %d: أيام\n - %h: ساعات\n - %m: دقائق\n - %s: ثواني\n - %w: الوقت المتبقي" + "name": "\u062a\u0646\u0633\u064a\u0642 \u0627\u0646\u062a\u0647\u0627\u0621 \u0627\u0644\u0633\u062a\u0631\u064a\u0643 \u0627\u0644\u0645\u062e\u0635\u0635", + "description": "\u064a\u062e\u0635\u0635 \u062a\u0646\u0633\u064a\u0642 \u0627\u0646\u062a\u0647\u0627\u0621 \u0627\u0644\u0633\u062a\u0631\u064a\u0643\n\n\u0627\u0644\u0645\u062a\u063a\u064a\u0631\u0627\u062a \u0627\u0644\u0645\u062a\u0627\u062d\u0629:\n - %c: \u0639\u062f\u062f \u0627\u0644\u0633\u062a\u0631\u064a\u0643\n - %e: \u0625\u064a\u0645\u0648\u062c\u064a \u0627\u0644\u0633\u0627\u0639\u0629 \u0627\u0644\u0631\u0645\u0644\u064a\u0629\n - %d: \u0623\u064a\u0627\u0645\n - %h: \u0633\u0627\u0639\u0627\u062a\n - %m: \u062f\u0642\u0627\u0626\u0642\n - %s: \u062b\u0648\u0627\u0646\u064a\n - %w: \u0627\u0644\u0648\u0642\u062a \u0627\u0644\u0645\u062a\u0628\u0642\u064a" }, "prevent_forced_logout": { - "name": "منع تسجيل الخروج الإجباري", - "description": "يمنع Snapchat من تسجيل خروجك عند تسجيل الدخول على جهاز آخر" + "name": "\u0645\u0646\u0639 \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062e\u0631\u0648\u062c \u0627\u0644\u0625\u062c\u0628\u0627\u0631\u064a", + "description": "\u064a\u0645\u0646\u0639 Snapchat \u0645\u0646 \u062a\u0633\u062c\u064a\u0644 \u062e\u0631\u0648\u062c\u0643 \u0639\u0646\u062f \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644 \u0639\u0644\u0649 \u062c\u0647\u0627\u0632 \u0622\u062e\u0631" }, "snapscore_changes": { - "name": "تغييرات نقاط Snap", - "description": "يتتبع التغييرات في نقاط Snap للأصدقاء\nاستخدم هذه الميزة في الإصدارات الأحدث من Snapchat فقط" + "name": "\u062a\u063a\u064a\u064a\u0631\u0627\u062a \u0646\u0642\u0627\u0637 Snap", + "description": "\u064a\u062a\u062a\u0628\u0639 \u0627\u0644\u062a\u063a\u064a\u064a\u0631\u0627\u062a \u0641\u064a \u0646\u0642\u0627\u0637 Snap \u0644\u0644\u0623\u0635\u062f\u0642\u0627\u0621\n\u0627\u0633\u062a\u062e\u062f\u0645 \u0647\u0630\u0647 \u0627\u0644\u0645\u064a\u0632\u0629 \u0641\u064a \u0627\u0644\u0625\u0635\u062f\u0627\u0631\u0627\u062a \u0627\u0644\u0623\u062d\u062f\u062b \u0645\u0646 Snapchat \u0641\u0642\u0637" } } }, "scripting": { - "name": "البرمجة (Scripting)", - "description": "تشغيل سكربتات مخصصة لتوسيع PurrfectSnap", + "name": "\u0627\u0644\u0628\u0631\u0645\u062c\u0629 (Scripting)", + "description": "\u062a\u0634\u063a\u064a\u0644 \u0633\u0643\u0631\u0628\u062a\u0627\u062a \u0645\u062e\u0635\u0635\u0629 \u0644\u062a\u0648\u0633\u064a\u0639 PurrfectSnap", "properties": { "developer_mode": { - "name": "وضع المطور", - "description": "يعرض معلومات التصحيح على واجهة Snapchat" + "name": "\u0648\u0636\u0639 \u0627\u0644\u0645\u0637\u0648\u0631", + "description": "\u064a\u0639\u0631\u0636 \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u062a\u0635\u062d\u064a\u062d \u0639\u0644\u0649 \u0648\u0627\u062c\u0647\u0629 Snapchat" }, "module_folder": { - "name": "مجلد الوحدات", - "description": "المجلد الذي توجد فيه السكربتات" + "name": "\u0645\u062c\u0644\u062f \u0627\u0644\u0648\u062d\u062f\u0627\u062a", + "description": "\u0627\u0644\u0645\u062c\u0644\u062f \u0627\u0644\u0630\u064a \u062a\u0648\u062c\u062f \u0641\u064a\u0647 \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a" }, "auto_reload": { - "name": "إعادة تحميل تلقائي", - "description": "يعيد تحميل السكربتات تلقائياً عند تغييرها" + "name": "\u0625\u0639\u0627\u062f\u0629 \u062a\u062d\u0645\u064a\u0644 \u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u064a\u0639\u064a\u062f \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0639\u0646\u062f \u062a\u063a\u064a\u064a\u0631\u0647\u0627" }, "integrated_ui": { - "name": "واجهة مستخدم مدمجة", - "description": "تسمح للسكربتات بإضافة مكونات واجهة مستخدم مخصصة لـ Snapchat" + "name": "\u0648\u0627\u062c\u0647\u0629 \u0645\u0633\u062a\u062e\u062f\u0645 \u0645\u062f\u0645\u062c\u0629", + "description": "\u062a\u0633\u0645\u062d \u0644\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a \u0628\u0625\u0636\u0627\u0641\u0629 \u0645\u0643\u0648\u0646\u0627\u062a \u0648\u0627\u062c\u0647\u0629 \u0645\u0633\u062a\u062e\u062f\u0645 \u0645\u062e\u0635\u0635\u0629 \u0644\u0640 Snapchat" }, "disable_log_anonymization": { - "name": "تعطيل إخفاء هوية السجل", - "description": "يعطل إخفاء الهوية في السجلات" + "name": "\u062a\u0639\u0637\u064a\u0644 \u0625\u062e\u0641\u0627\u0621 \u0647\u0648\u064a\u0629 \u0627\u0644\u0633\u062c\u0644", + "description": "\u064a\u0639\u0637\u0644 \u0625\u062e\u0641\u0627\u0621 \u0627\u0644\u0647\u0648\u064a\u0629 \u0641\u064a \u0627\u0644\u0633\u062c\u0644\u0627\u062a" }, "disable_optimization": { - "name": "تعطيل التحسين", - "description": "يعطل تحسين السكربتات. قد يسبب هذا مشاكل في الأداء." + "name": "\u062a\u0639\u0637\u064a\u0644 \u0627\u0644\u062a\u062d\u0633\u064a\u0646", + "description": "\u064a\u0639\u0637\u0644 \u062a\u062d\u0633\u064a\u0646 \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a. \u0642\u062f \u064a\u0633\u0628\u0628 \u0647\u0630\u0627 \u0645\u0634\u0627\u0643\u0644 \u0641\u064a \u0627\u0644\u0623\u062f\u0627\u0621." } } }, "friend_tracker": { - "name": "متتبع الأصدقاء", - "description": "يسجل نشاط الصديق على Snapchat", + "name": "\u0645\u062a\u062a\u0628\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "description": "\u064a\u0633\u062c\u0644 \u0646\u0634\u0627\u0637 \u0627\u0644\u0635\u062f\u064a\u0642 \u0639\u0644\u0649 Snapchat", "properties": { "record_messaging_events": { - "name": "تسجيل أحداث المراسلة", - "description": "يسجل أحداث المراسلة مثل فتح snap، قراءة رسالة، إلخ." + "name": "\u062a\u0633\u062c\u064a\u0644 \u0623\u062d\u062f\u0627\u062b \u0627\u0644\u0645\u0631\u0627\u0633\u0644\u0629", + "description": "\u064a\u0633\u062c\u0644 \u0623\u062d\u062f\u0627\u062b \u0627\u0644\u0645\u0631\u0627\u0633\u0644\u0629 \u0645\u062b\u0644 \u0641\u062a\u062d snap\u060c \u0642\u0631\u0627\u0621\u0629 \u0631\u0633\u0627\u0644\u0629\u060c \u0625\u0644\u062e." }, "allow_running_in_background": { - "name": "السماح بالتشغيل في الخلفية", - "description": "يسمح للمتتبع بالعمل في الخلفية. ملاحظة: هذا سيستنزف بطاريتك بشكل كبير" + "name": "\u0627\u0644\u0633\u0645\u0627\u062d \u0628\u0627\u0644\u062a\u0634\u063a\u064a\u0644 \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629", + "description": "\u064a\u0633\u0645\u062d \u0644\u0644\u0645\u062a\u062a\u0628\u0639 \u0628\u0627\u0644\u0639\u0645\u0644 \u0641\u064a \u0627\u0644\u062e\u0644\u0641\u064a\u0629. \u0645\u0644\u0627\u062d\u0638\u0629: \u0647\u0630\u0627 \u0633\u064a\u0633\u062a\u0646\u0632\u0641 \u0628\u0637\u0627\u0631\u064a\u062a\u0643 \u0628\u0634\u0643\u0644 \u0643\u0628\u064a\u0631" }, "auto_purge": { - "name": "تطهير تلقائي", - "description": "يحذف الأحداث المخبأة تلقائياً التي أقدم من المدة المحددة" + "name": "\u062a\u0637\u0647\u064a\u0631 \u062a\u0644\u0642\u0627\u0626\u064a", + "description": "\u064a\u062d\u0630\u0641 \u0627\u0644\u0623\u062d\u062f\u0627\u062b \u0627\u0644\u0645\u062e\u0628\u0623\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0627\u0644\u062a\u064a \u0623\u0642\u062f\u0645 \u0645\u0646 \u0627\u0644\u0645\u062f\u0629 \u0627\u0644\u0645\u062d\u062f\u062f\u0629" } } } }, "options": { - "empty": "فارغ", + "empty": "\u0641\u0627\u0631\u063a", "location_search_provider": { - "osm": "OpenStreetMap (مجاني)", - "google_maps": "خرائط Google" + "osm": "OpenStreetMap (\u0645\u062c\u0627\u0646\u064a)", + "google_maps": "\u062e\u0631\u0627\u0626\u0637 Google" }, "unsaveable_messages": { - "blacklist": "وضع القائمة السوداء", - "whitelist": "وضع القائمة البيضاء", - "null": "معطل" + "blacklist": "\u0648\u0636\u0639 \u0627\u0644\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0633\u0648\u062f\u0627\u0621", + "whitelist": "\u0648\u0636\u0639 \u0627\u0644\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0628\u064a\u0636\u0627\u0621", + "null": "\u0645\u0639\u0637\u0644" }, "app_appearance": { - "always_light": "فاتح دائماً", - "always_dark": "داكن دائماً", - "null": "مطابقة النظام" + "always_light": "\u0641\u0627\u062a\u062d \u062f\u0627\u0626\u0645\u0627\u064b", + "always_dark": "\u062f\u0627\u0643\u0646 \u062f\u0627\u0626\u0645\u0627\u064b", + "null": "\u0645\u0637\u0627\u0628\u0642\u0629 \u0627\u0644\u0646\u0638\u0627\u0645" }, "auto_reload": { - "snapchat_only": "إعادة تحميل Snapchat فقط", - "all": "إعادة تحميل Snapchat + PurrfectSnap", - "null": "افتراضي" + "snapchat_only": "\u0625\u0639\u0627\u062f\u0629 \u062a\u062d\u0645\u064a\u0644 Snapchat \u0641\u0642\u0637", + "all": "\u0625\u0639\u0627\u062f\u0629 \u062a\u062d\u0645\u064a\u0644 Snapchat + PurrfectSnap", + "null": "\u0627\u0641\u062a\u0631\u0627\u0636\u064a" }, "walk_radius": { - "null": "استخدام نصف القطر الافتراضي" + "null": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0646\u0635\u0641 \u0627\u0644\u0642\u0637\u0631 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a" }, "spoof_battery_level": { - "null": "استخدام مستوى البطارية الحقيقي" + "null": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0645\u0633\u062a\u0648\u0649 \u0627\u0644\u0628\u0637\u0627\u0631\u064a\u0629 \u0627\u0644\u062d\u0642\u064a\u0642\u064a" }, "friend_feed_menu_buttons": { - "auto_download": "\u2b07\ufe0f تنزيل تلقائي", - "auto_save": "\ud83d\udcac حفظ الرسائل تلقائياً", - "unsaveable_messages": "\u2b07\ufe0f رسائل غير قابلة للحفظ", - "auto_open_snaps": "\ud83d\udcf7 فتح الـ Snaps تلقائياً", - "stealth": "\ud83d\udc7b وضع التخفي", - "auto_reply": "\ud83d\udce8 الرد التلقائي", - "auto_delete_sent_messages": "\ud83d\uddd1\ufe0f حذف الرسائل المرسلة تلقائياً", - "mark_snaps_as_seen": "\ud83d\udc40 وضع علامة \"تمت المشاهدة\" على Snaps", - "mark_stories_as_seen_locally": "\ud83d\udc40 وضع علامة \"تمت المشاهدة\" على القصص محلياً", - "conversation_info": "\ud83d\udc64 معلومات المحادثة", - "e2e_encryption": "\ud83d\udd12 استخدام التشفير من طرف لطرف", - "message_logger": "\ud83d\udcdd مسجل الرسائل", - "auto_read": "\u2705 قراءة تلقائية", - "hide_typing_indicator": "\ud83d\ude48 إخفاء مؤشر الكتابة" + "auto_download": "\u2b07\ufe0f \u062a\u0646\u0632\u064a\u0644 \u062a\u0644\u0642\u0627\u0626\u064a", + "auto_save": "\ud83d\udcac \u062d\u0641\u0638 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "unsaveable_messages": "\u2b07\ufe0f \u0631\u0633\u0627\u0626\u0644 \u063a\u064a\u0631 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u062d\u0641\u0638", + "auto_open_snaps": "\ud83d\udcf7 \u0641\u062a\u062d \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "stealth": "\ud83d\udc7b \u0648\u0636\u0639 \u0627\u0644\u062a\u062e\u0641\u064a", + "snap_stealth": "\ud83d\udcf7 \u0648\u0636\u0639 \u062a\u062e\u0641\u064a \u0627\u0644\u0633\u0646\u0627\u0628", + "chat_stealth": "\ud83d\udcac \u0648\u0636\u0639 \u062a\u062e\u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "auto_reply": "\ud83d\udce8 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "auto_delete_sent_messages": "\ud83d\uddd1\ufe0f \u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0631\u0633\u0644\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "mark_snaps_as_seen": "\ud83d\udc40 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\" \u0639\u0644\u0649 Snaps", + "mark_stories_as_seen_locally": "\ud83d\udc40 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\" \u0639\u0644\u0649 \u0627\u0644\u0642\u0635\u0635 \u0645\u062d\u0644\u064a\u0627\u064b", + "conversation_info": "\ud83d\udc64 \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "e2e_encryption": "\ud83d\udd12 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u062a\u0634\u0641\u064a\u0631 \u0645\u0646 \u0637\u0631\u0641 \u0644\u0637\u0631\u0641", + "message_logger": "\ud83d\udcdd \u0645\u0633\u062c\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "auto_read": "\u2705 \u0642\u0631\u0627\u0621\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0629", + "hide_typing_indicator": "\ud83d\ude48 \u0625\u062e\u0641\u0627\u0621 \u0645\u0624\u0634\u0631 \u0627\u0644\u0643\u062a\u0627\u0628\u0629" }, - "schedule_scheduled_for": "مجدول لـ {name} في {time}", - "schedule_sending_in": "إرسال خلال {time}", - "schedule_sent_to": "تم الإرسال إلى {name}", - "schedule_sent": "تم إرسال الـ snap المجدول", - "schedule_failed_to": "فشل الإرسال إلى {name}", - "schedule_failed": "فشل الـ snap المجدول", - "schedule_cancelled_for": "تم الإلغاء لـ {name}", + "schedule_scheduled_for": "\u0645\u062c\u062f\u0648\u0644 \u0644\u0640 {name} \u0641\u064a {time}", + "schedule_sending_in": "\u0625\u0631\u0633\u0627\u0644 \u062e\u0644\u0627\u0644 {time}", + "schedule_sent_to": "\u062a\u0645 \u0627\u0644\u0625\u0631\u0633\u0627\u0644 \u0625\u0644\u0649 {name}", + "schedule_sent": "\u062a\u0645 \u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0640 snap \u0627\u0644\u0645\u062c\u062f\u0648\u0644", + "schedule_failed_to": "\u0641\u0634\u0644 \u0627\u0644\u0625\u0631\u0633\u0627\u0644 \u0625\u0644\u0649 {name}", + "schedule_failed": "\u0641\u0634\u0644 \u0627\u0644\u0640 snap \u0627\u0644\u0645\u062c\u062f\u0648\u0644", + "schedule_cancelled_for": "\u062a\u0645 \u0627\u0644\u0625\u0644\u063a\u0627\u0621 \u0644\u0640 {name}", "device_model": { "samsung_s25_ultra": "Samsung Galaxy S25 Ultra", "google_pixel_10_pro": "Google Pixel 10 Pro", "oneplus_13": "OnePlus 13", "xiaomi_15_ultra": "Xiaomi 15 Ultra", - "null": "افتراضي الجهاز" + "null": "\u0627\u0641\u062a\u0631\u0627\u0636\u064a \u0627\u0644\u062c\u0647\u0627\u0632" }, "settings_menu": { - "default": "افتراضي", - "legacy": "قديم (Legacy)" + "default": "\u0627\u0641\u062a\u0631\u0627\u0636\u064a", + "legacy": "\u0642\u062f\u064a\u0645 (Legacy)" }, "path_format": { - "create_author_folder": "إنشاء مجلد لكل مؤلف", - "create_source_folder": "إنشاء مجلد لكل نوع مصدر وسائط", - "append_hash": "إضافة هاش فريد لاسم الملف", - "append_source": "إضافة مصدر الوسائط لاسم الملف", - "append_username": "إضافة اسم المستخدم لاسم الملف", - "append_date_time": "إضافة التاريخ والوقت لاسم الملف", - "append_type": "إضافة نوع الوسائط لاسم الملف" + "create_author_folder": "\u0625\u0646\u0634\u0627\u0621 \u0645\u062c\u0644\u062f \u0644\u0643\u0644 \u0645\u0624\u0644\u0641", + "create_source_folder": "\u0625\u0646\u0634\u0627\u0621 \u0645\u062c\u0644\u062f \u0644\u0643\u0644 \u0646\u0648\u0639 \u0645\u0635\u062f\u0631 \u0648\u0633\u0627\u0626\u0637", + "append_hash": "\u0625\u0636\u0627\u0641\u0629 \u0647\u0627\u0634 \u0641\u0631\u064a\u062f \u0644\u0627\u0633\u0645 \u0627\u0644\u0645\u0644\u0641", + "append_source": "\u0625\u0636\u0627\u0641\u0629 \u0645\u0635\u062f\u0631 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0644\u0627\u0633\u0645 \u0627\u0644\u0645\u0644\u0641", + "append_username": "\u0625\u0636\u0627\u0641\u0629 \u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0644\u0627\u0633\u0645 \u0627\u0644\u0645\u0644\u0641", + "append_date_time": "\u0625\u0636\u0627\u0641\u0629 \u0627\u0644\u062a\u0627\u0631\u064a\u062e \u0648\u0627\u0644\u0648\u0642\u062a \u0644\u0627\u0633\u0645 \u0627\u0644\u0645\u0644\u0641", + "append_type": "\u0625\u0636\u0627\u0641\u0629 \u0646\u0648\u0639 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0644\u0627\u0633\u0645 \u0627\u0644\u0645\u0644\u0641" }, "auto_download_sources": { - "friend_snaps": "Snaps الأصدقاء", - "friend_stories": "قصص الأصدقاء", - "public_stories": "القصص العامة", + "friend_snaps": "Snaps \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "friend_stories": "\u0642\u0635\u0635 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "public_stories": "\u0627\u0644\u0642\u0635\u0635 \u0627\u0644\u0639\u0627\u0645\u0629", "spotlight": "Spotlight" }, "logging": { - "started": "بدأ", - "success": "نجاح", - "progress": "تقدم", - "failure": "فشل" + "started": "\u0628\u062f\u0623", + "success": "\u0646\u062c\u0627\u062d", + "progress": "\u062a\u0642\u062f\u0645", + "failure": "\u0641\u0634\u0644" }, "notifications": { - "chat_screenshot": "لقطة شاشة", - "chat_screen_record": "تسجيل الشاشة", - "snap_replay": "إعادة تشغيل Snap", - "camera_roll_save": "حفظ ألبوم الكاميرا", - "chat": "دردشة", - "chat_reply": "رد دردشة", + "chat_screenshot": "\u0644\u0642\u0637\u0629 \u0634\u0627\u0634\u0629", + "chat_screen_record": "\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u0634\u0627\u0634\u0629", + "snap_replay": "\u0625\u0639\u0627\u062f\u0629 \u062a\u0634\u063a\u064a\u0644 Snap", + "camera_roll_save": "\u062d\u0641\u0638 \u0623\u0644\u0628\u0648\u0645 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627", + "chat": "\u062f\u0631\u062f\u0634\u0629", + "chat_reply": "\u0631\u062f \u062f\u0631\u062f\u0634\u0629", "snap": "Snap", - "typing": "جاري الكتابة", - "stories": "قصص", - "speaking": "يتحدث", - "chat_reaction": "تفاعل DM", - "group_chat_reaction": "تفاعل المجموعة", - "initiate_audio": "مكالمة صوتية واردة", - "abandon_audio": "مكالمة صوتية فائتة", - "initiate_video": "مكالمة فيديو واردة", - "abandon_video": "مكالمة فيديو فائتة", - "map_live_location": "موقع مباشر على الخريطة" + "typing": "\u062c\u0627\u0631\u064a \u0627\u0644\u0643\u062a\u0627\u0628\u0629", + "stories": "\u0642\u0635\u0635", + "speaking": "\u064a\u062a\u062d\u062f\u062b", + "chat_reaction": "\u062a\u0641\u0627\u0639\u0644 DM", + "group_chat_reaction": "\u062a\u0641\u0627\u0639\u0644 \u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0629", + "initiate_audio": "\u0645\u0643\u0627\u0644\u0645\u0629 \u0635\u0648\u062a\u064a\u0629 \u0648\u0627\u0631\u062f\u0629", + "abandon_audio": "\u0645\u0643\u0627\u0644\u0645\u0629 \u0635\u0648\u062a\u064a\u0629 \u0641\u0627\u0626\u062a\u0629", + "initiate_video": "\u0645\u0643\u0627\u0644\u0645\u0629 \u0641\u064a\u062f\u064a\u0648 \u0648\u0627\u0631\u062f\u0629", + "abandon_video": "\u0645\u0643\u0627\u0644\u0645\u0629 \u0641\u064a\u062f\u064a\u0648 \u0641\u0627\u0626\u062a\u0629", + "map_live_location": "\u0645\u0648\u0642\u0639 \u0645\u0628\u0627\u0634\u0631 \u0639\u0644\u0649 \u0627\u0644\u062e\u0631\u064a\u0637\u0629" }, "auto_read": { - "blacklist": "قائمة سوداء", - "whitelist": "قائمة بيضاء", - "disabled": "معطل" + "blacklist": "\u0642\u0627\u0626\u0645\u0629 \u0633\u0648\u062f\u0627\u0621", + "whitelist": "\u0642\u0627\u0626\u0645\u0629 \u0628\u064a\u0636\u0627\u0621", + "disabled": "\u0645\u0639\u0637\u0644" }, "hide_typing_indicator": { - "blacklist": "قائمة سوداء", - "whitelist": "قائمة بيضاء", - "disabled": "معطل" + "blacklist": "\u0642\u0627\u0626\u0645\u0629 \u0633\u0648\u062f\u0627\u0621", + "whitelist": "\u0642\u0627\u0626\u0645\u0629 \u0628\u064a\u0636\u0627\u0621", + "disabled": "\u0645\u0639\u0637\u0644" }, "auto_delete_sent_messages": { - "blacklist": "قائمة سوداء", - "whitelist": "قائمة بيضاء", - "disabled": "معطل" + "blacklist": "\u0642\u0627\u0626\u0645\u0629 \u0633\u0648\u062f\u0627\u0621", + "whitelist": "\u0642\u0627\u0626\u0645\u0629 \u0628\u064a\u0636\u0627\u0621", + "disabled": "\u0645\u0639\u0637\u0644" }, "auto_download": { - "blacklist": "قائمة سوداء", - "whitelist": "قائمة بيضاء", - "disabled": "معطل" + "blacklist": "\u0642\u0627\u0626\u0645\u0629 \u0633\u0648\u062f\u0627\u0621", + "whitelist": "\u0642\u0627\u0626\u0645\u0629 \u0628\u064a\u0636\u0627\u0621", + "disabled": "\u0645\u0639\u0637\u0644" }, "stealth": { - "blacklist": "قائمة سوداء", - "whitelist": "قائمة بيضاء", - "disabled": "معطل" + "blacklist": "\u0642\u0627\u0626\u0645\u0629 \u0633\u0648\u062f\u0627\u0621", + "whitelist": "\u0642\u0627\u0626\u0645\u0629 \u0628\u064a\u0636\u0627\u0621", + "disabled": "\u0645\u0639\u0637\u0644" }, "auto_save": { - "blacklist": "قائمة سوداء", - "whitelist": "قائمة بيضاء", - "disabled": "معطل" + "blacklist": "\u0642\u0627\u0626\u0645\u0629 \u0633\u0648\u062f\u0627\u0621", + "whitelist": "\u0642\u0627\u0626\u0645\u0629 \u0628\u064a\u0636\u0627\u0621", + "disabled": "\u0645\u0639\u0637\u0644" }, "message_logger": { - "blacklist": "قائمة سوداء", - "whitelist": "قائمة بيضاء", - "disabled": "معطل" + "blacklist": "\u0642\u0627\u0626\u0645\u0629 \u0633\u0648\u062f\u0627\u0621", + "whitelist": "\u0642\u0627\u0626\u0645\u0629 \u0628\u064a\u0636\u0627\u0621", + "disabled": "\u0645\u0639\u0637\u0644" }, "auto_reply": { - "blacklist": "قائمة سوداء", - "whitelist": "قائمة بيضاء", - "disabled": "معطل" + "blacklist": "\u0642\u0627\u0626\u0645\u0629 \u0633\u0648\u062f\u0627\u0621", + "whitelist": "\u0642\u0627\u0626\u0645\u0629 \u0628\u064a\u0636\u0627\u0621", + "disabled": "\u0645\u0639\u0637\u0644" }, "custom_android_id": { - "null": "استخدام معرف Android الحقيقي" + "null": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0645\u0639\u0631\u0641 Android \u0627\u0644\u062d\u0642\u064a\u0642\u064a" + }, + "add_friend_source_spoof": { + "added_by_username": "\u0628\u0648\u0627\u0633\u0637\u0629 \u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "added_by_mention": "\u0628\u0648\u0627\u0633\u0637\u0629 \u0627\u0644\u0625\u0634\u0627\u0631\u0629 (Mention)", + "added_by_group_chat": "\u0628\u0648\u0627\u0633\u0637\u0629 \u062f\u0631\u062f\u0634\u0629 \u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0629", + "added_by_qr_code": "\u0628\u0648\u0627\u0633\u0637\u0629 \u0631\u0645\u0632 QR", + "added_by_community": "\u0628\u0648\u0627\u0633\u0637\u0629 \u0627\u0644\u0645\u062c\u062a\u0645\u0639", + "added_by_quick_add": "\u0628\u0648\u0627\u0633\u0637\u0629 \u0627\u0644\u0625\u0636\u0627\u0641\u0629 \u0627\u0644\u0633\u0631\u064a\u0639\u0629 (\u062e\u0637\u0631 \u0639\u0627\u0644\u064a \u0644\u0644\u062d\u0638\u0631)", + "added_by_spotlight": "\u0628\u0648\u0627\u0633\u0637\u0629 Spotlight", + "null": "\u0639\u062f\u0645 \u062a\u0632\u064a\u064a\u0641 \u0627\u0644\u0645\u0635\u062f\u0631" }, - "add_friend_source_spoof": { - "added_by_username": "بواسطة اسم المستخدم", - "added_by_mention": "بواسطة الإشارة (Mention)", - "added_by_group_chat": "بواسطة دردشة المجموعة", - "added_by_qr_code": "بواسطة رمز QR", - "added_by_community": "بواسطة المجتمع", - "added_by_quick_add": "بواسطة الإضافة السريعة (خطر عالي للحظر)", - "added_by_spotlight": "بواسطة Spotlight", - "null": "عدم تزييف المصدر" - }, "custom_streaks_expiration_format": { - "null": "الافتراضي للنظام" + "null": "\u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a \u0644\u0644\u0646\u0638\u0627\u0645" }, "preferred_transcription_lang": { - "null": "استخدام افتراضي Snapchat" + "null": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0641\u062a\u0631\u0627\u0636\u064a Snapchat" }, "custom_emoji_font": { - "null": "خط الإيموجي الافتراضي" + "null": "\u062e\u0637 \u0627\u0644\u0625\u064a\u0645\u0648\u062c\u064a \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a" }, "custom_shared_library": { - "null": "استخدام المكتبة الافتراضية" + "null": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u0645\u0643\u062a\u0628\u0629 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a\u0629" }, "override_front_resolution": { - "null": "استخدام افتراضي الجهاز" + "null": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0641\u062a\u0631\u0627\u0636\u064a \u0627\u0644\u062c\u0647\u0627\u0632" }, "override_back_resolution": { - "null": "استخدام افتراضي الجهاز" + "null": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0641\u062a\u0631\u0627\u0636\u064a \u0627\u0644\u062c\u0647\u0627\u0632" }, "custom_resolution": { - "null": "استخدام الدقة التلقائية" + "null": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u062f\u0642\u0629 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a\u0629" }, "startup_default_camera": { - "front": "الكاميرا الأمامية", - "back": "الكاميرا الخلفية", - "null": "تذكر آخر مستخدمة" + "front": "\u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u0623\u0645\u0627\u0645\u064a\u0629", + "back": "\u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u062e\u0644\u0641\u064a\u0629", + "null": "\u062a\u0630\u0643\u0631 \u0622\u062e\u0631 \u0645\u0633\u062a\u062e\u062f\u0645\u0629" }, "call_recorder": { - "only_record_self": "تسجيل النفس فقط", - "only_record_others": "تسجيل الآخرين فقط", - "record_both": "تسجيل كلا الطرفين" + "only_record_self": "\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u0646\u0641\u0633 \u0641\u0642\u0637", + "only_record_others": "\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u0622\u062e\u0631\u064a\u0646 \u0641\u0642\u0637", + "record_both": "\u062a\u0633\u062c\u064a\u0644 \u0643\u0644\u0627 \u0627\u0644\u0637\u0631\u0641\u064a\u0646" }, "call_recorder_ui_design": { - "default": "افتراضي", + "default": "\u0627\u0641\u062a\u0631\u0627\u0636\u064a", "snapchat": "Snapchat", "cyber": "Cyber", "frost": "Frost" }, "front_custom_frame_rate": { - "null": "FPS الافتراضي للجهاز" + "null": "FPS \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a \u0644\u0644\u062c\u0647\u0627\u0632" }, "back_custom_frame_rate": { - "null": "FPS الافتراضي للجهاز" + "null": "FPS \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a \u0644\u0644\u062c\u0647\u0627\u0632" }, "force_voice_note_format": { - "null": "استخدام افتراضي Snapchat" + "null": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0641\u062a\u0631\u0627\u0636\u064a Snapchat" }, "custom_path_format": { - "null": "استخدام النمط الافتراضي" + "null": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u0646\u0645\u0637 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a" }, "force_image_format": { - "null": "استخدام افتراضي Snapchat" + "null": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0641\u062a\u0631\u0627\u0636\u064a Snapchat" }, "custom_video_codec": { - "null": "الترميز الافتراضي" + "null": "\u0627\u0644\u062a\u0631\u0645\u064a\u0632 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a" }, "custom_audio_codec": { - "null": "الترميز الافتراضي" + "null": "\u0627\u0644\u062a\u0631\u0645\u064a\u0632 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a" }, "preset": { - "null": "الإعداد المسبق الافتراضي" + "null": "\u0627\u0644\u0625\u0639\u062f\u0627\u062f \u0627\u0644\u0645\u0633\u0628\u0642 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a" }, "app_appearance_override": { - "title": "المظهر" + "title": "\u0627\u0644\u0645\u0638\u0647\u0631" }, "gallery_media_send_override": { - "always_ask": "اسأل دائماً", - "ORIGINAL": "الوسائط الأصلية", - "NOTE": "ملاحظة صوتية", + "always_ask": "\u0627\u0633\u0623\u0644 \u062f\u0627\u0626\u0645\u0627\u064b", + "ORIGINAL": "\u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u0623\u0635\u0644\u064a\u0629", + "NOTE": "\u0645\u0644\u0627\u062d\u0638\u0629 \u0635\u0648\u062a\u064a\u0629", "SNAP": "Snap", - "SAVEABLE_SNAP": "Snap قابل للحفظ", - "null": "افتراضي Snapchat", - "multiple_media_toast": "يمكنك إرسال وسيط واحد فقط في المرة الواحدة" + "SAVEABLE_SNAP": "Snap \u0642\u0627\u0628\u0644 \u0644\u0644\u062d\u0641\u0638", + "null": "\u0627\u0641\u062a\u0631\u0627\u0636\u064a Snapchat", + "multiple_media_toast": "\u064a\u0645\u0643\u0646\u0643 \u0625\u0631\u0633\u0627\u0644 \u0648\u0633\u064a\u0637 \u0648\u0627\u062d\u062f \u0641\u0642\u0637 \u0641\u064a \u0627\u0644\u0645\u0631\u0629 \u0627\u0644\u0648\u0627\u062d\u062f\u0629" }, "strip_media_metadata": { - "hide_caption_text": "إخفاء نص التسمية التوضيحية", - "hide_snap_filters": "إخفاء فلاتر Snap", - "hide_extras": "إخفاء الإضافات (مثل الإشارات)", - "remove_audio_note_duration": "إزالة مدة الملاحظة الصوتية", - "remove_audio_note_transcript_capability": "إزالة قدرة نسخ الملاحظة الصوتية" + "hide_caption_text": "\u0625\u062e\u0641\u0627\u0621 \u0646\u0635 \u0627\u0644\u062a\u0633\u0645\u064a\u0629 \u0627\u0644\u062a\u0648\u0636\u064a\u062d\u064a\u0629", + "hide_snap_filters": "\u0625\u062e\u0641\u0627\u0621 \u0641\u0644\u0627\u062a\u0631 Snap", + "hide_extras": "\u0625\u062e\u0641\u0627\u0621 \u0627\u0644\u0625\u0636\u0627\u0641\u0627\u062a (\u0645\u062b\u0644 \u0627\u0644\u0625\u0634\u0627\u0631\u0627\u062a)", + "remove_audio_note_duration": "\u0625\u0632\u0627\u0644\u0629 \u0645\u062f\u0629 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0629 \u0627\u0644\u0635\u0648\u062a\u064a\u0629", + "remove_audio_note_transcript_capability": "\u0625\u0632\u0627\u0644\u0629 \u0642\u062f\u0631\u0629 \u0646\u0633\u062e \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0629 \u0627\u0644\u0635\u0648\u062a\u064a\u0629" }, "hide_ui_components": { - "hide_profile_call_buttons": "إزالة أزرار الاتصال في الملف الشخصي", - "hide_chat_call_buttons": "إزالة أزرار الاتصال في الدردشة", - "hide_live_location_share_button": "إزالة زر مشاركة الموقع المباشر", - "hide_stickers_button": "إزالة زر الملصقات", - "hide_voice_record_button": "إزالة زر التسجيل الصوتي", - "hide_unread_chat_hint": "إزالة تلميح الدردشة غير المقروءة", - "hide_post_to_story_buttons": "إزالة أزرار النشر في القصة قبل إرسال Snap", - "hide_billboard_prompt": "إزالة موجه اللوحة الإعلانية في موجز الأصدقاء", - "hide_snapchat_plus_gift_reminders": "إزالة تذكيرات هدايا Snapchat Plus في المحادثات", - "hide_map_reactions": "إزالة تفاعلات الخريطة" + "hide_profile_call_buttons": "\u0625\u0632\u0627\u0644\u0629 \u0623\u0632\u0631\u0627\u0631 \u0627\u0644\u0627\u062a\u0635\u0627\u0644 \u0641\u064a \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a", + "hide_chat_call_buttons": "\u0625\u0632\u0627\u0644\u0629 \u0623\u0632\u0631\u0627\u0631 \u0627\u0644\u0627\u062a\u0635\u0627\u0644 \u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "hide_live_location_share_button": "\u0625\u0632\u0627\u0644\u0629 \u0632\u0631 \u0645\u0634\u0627\u0631\u0643\u0629 \u0627\u0644\u0645\u0648\u0642\u0639 \u0627\u0644\u0645\u0628\u0627\u0634\u0631", + "hide_stickers_button": "\u0625\u0632\u0627\u0644\u0629 \u0632\u0631 \u0627\u0644\u0645\u0644\u0635\u0642\u0627\u062a", + "hide_voice_record_button": "\u0625\u0632\u0627\u0644\u0629 \u0632\u0631 \u0627\u0644\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u0635\u0648\u062a\u064a", + "hide_unread_chat_hint": "\u0625\u0632\u0627\u0644\u0629 \u062a\u0644\u0645\u064a\u062d \u0627\u0644\u062f\u0631\u062f\u0634\u0629 \u063a\u064a\u0631 \u0627\u0644\u0645\u0642\u0631\u0648\u0621\u0629", + "hide_post_to_story_buttons": "\u0625\u0632\u0627\u0644\u0629 \u0623\u0632\u0631\u0627\u0631 \u0627\u0644\u0646\u0634\u0631 \u0641\u064a \u0627\u0644\u0642\u0635\u0629 \u0642\u0628\u0644 \u0625\u0631\u0633\u0627\u0644 Snap", + "hide_billboard_prompt": "\u0625\u0632\u0627\u0644\u0629 \u0645\u0648\u062c\u0647 \u0627\u0644\u0644\u0648\u062d\u0629 \u0627\u0644\u0625\u0639\u0644\u0627\u0646\u064a\u0629 \u0641\u064a \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "hide_snapchat_plus_gift_reminders": "\u0625\u0632\u0627\u0644\u0629 \u062a\u0630\u0643\u064a\u0631\u0627\u062a \u0647\u062f\u0627\u064a\u0627 Snapchat Plus \u0641\u064a \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0627\u062a", + "hide_map_reactions": "\u0625\u0632\u0627\u0644\u0629 \u062a\u0641\u0627\u0639\u0644\u0627\u062a \u0627\u0644\u062e\u0631\u064a\u0637\u0629" }, "hide_story_suggestions": { - "hide_suggested_friend_stories": "إخفاء قصص الأصدقاء المقترحة", - "hide_my_stories": "إخفاء قصصي" + "hide_suggested_friend_stories": "\u0625\u062e\u0641\u0627\u0621 \u0642\u0635\u0635 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0627\u0644\u0645\u0642\u062a\u0631\u062d\u0629", + "hide_my_stories": "\u0625\u062e\u0641\u0627\u0621 \u0642\u0635\u0635\u064a" }, "home_tab": { - "map": "الخريطة", - "chat": "الدردشة", - "camera": "الكاميرا", - "discover": "اكتشف (Discover)", + "map": "\u0627\u0644\u062e\u0631\u064a\u0637\u0629", + "chat": "\u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "camera": "\u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627", + "discover": "\u0627\u0643\u062a\u0634\u0641 (Discover)", "spotlight": "Spotlight", - "null": "افتراضي Snapchat" + "null": "\u0627\u0641\u062a\u0631\u0627\u0636\u064a Snapchat" }, "spotlight_comments_username_icon": { - "user": "أيقونة اسم المستخدم", - "\ud83d\udc64": "أيقونة اسم المستخدم", - "[\ud83d\udc64]": "أيقونة اسم المستخدم", - "default": "أيقونة اسم المستخدم", - "no_icon": "لا توجد أيقونة" + "user": "\u0623\u064a\u0642\u0648\u0646\u0629 \u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "\ud83d\udc64": "\u0623\u064a\u0642\u0648\u0646\u0629 \u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "[\ud83d\udc64]": "\u0623\u064a\u0642\u0648\u0646\u0629 \u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "default": "\u0623\u064a\u0642\u0648\u0646\u0629 \u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "no_icon": "\u0644\u0627 \u062a\u0648\u062c\u062f \u0623\u064a\u0642\u0648\u0646\u0629" }, "custom_image_upload_format": { - "null": "تلقائي" + "null": "\u062a\u0644\u0642\u0627\u0626\u064a" }, "update_check_frequency": { - "daily": "يومياً", - "weekly": "أسبوعياً", - "monthly": "شهرياً", - "null": "تلقائي" + "daily": "\u064a\u0648\u0645\u064a\u0627\u064b", + "weekly": "\u0623\u0633\u0628\u0648\u0639\u064a\u0627\u064b", + "monthly": "\u0634\u0647\u0631\u064a\u0627\u064b", + "null": "\u062a\u0644\u0642\u0627\u0626\u064a" }, "snapchat_plus": { - "not_subscribed": "غير مشترك", - "basic": "أساسي", - "ad_free": "خالي من الإعلانات", - "null": "افتراضي" + "not_subscribed": "\u063a\u064a\u0631 \u0645\u0634\u062a\u0631\u0643", + "basic": "\u0623\u0633\u0627\u0633\u064a", + "ad_free": "\u062e\u0627\u0644\u064a \u0645\u0646 \u0627\u0644\u0625\u0639\u0644\u0627\u0646\u0627\u062a", + "null": "\u0627\u0641\u062a\u0631\u0627\u0636\u064a" }, "bypass_video_length_restriction": { - "single": "وسيط فردي", - "split": "وسيط مقسم", - "null": "افتراضي" + "single": "\u0648\u0633\u064a\u0637 \u0641\u0631\u062f\u064a", + "split": "\u0648\u0633\u064a\u0637 \u0645\u0642\u0633\u0645", + "null": "\u0627\u0641\u062a\u0631\u0627\u0636\u064a" }, "old_bitmoji_selfie": { - "2d": "Bitmoji ثنائي الأبعاد (2D)", - "3d": "Bitmoji ثلاثي الأبعاد (3D)", - "null": "Bitmoji الافتراضي" + "2d": "Bitmoji \u062b\u0646\u0627\u0626\u064a \u0627\u0644\u0623\u0628\u0639\u0627\u062f (2D)", + "3d": "Bitmoji \u062b\u0644\u0627\u062b\u064a \u0627\u0644\u0623\u0628\u0639\u0627\u062f (3D)", + "null": "Bitmoji \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a" }, "disable_confirmation_dialogs": { - "erase_message": "محو الرسالة", - "remove_friend": "إزالة الصديق", - "block_friend": "حظر الصديق", - "ignore_friend": "تجاهل الصديق", - "hide_friend": "إخفاء الصديق", - "hide_conversation": "إخفاء المحادثة", - "clear_conversation": "مسح المحادثة من موجز الأصدقاء" + "erase_message": "\u0645\u062d\u0648 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "remove_friend": "\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0635\u062f\u064a\u0642", + "block_friend": "\u062d\u0638\u0631 \u0627\u0644\u0635\u062f\u064a\u0642", + "ignore_friend": "\u062a\u062c\u0627\u0647\u0644 \u0627\u0644\u0635\u062f\u064a\u0642", + "hide_friend": "\u0625\u062e\u0641\u0627\u0621 \u0627\u0644\u0635\u062f\u064a\u0642", + "hide_conversation": "\u0625\u062e\u0641\u0627\u0621 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "clear_conversation": "\u0645\u0633\u062d \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629 \u0645\u0646 \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621" }, "edit_text_override": { - "multi_line_chat_input": "إدخال دردشة متعدد الأسطر", - "bypass_text_input_limit": "تجاوز حد إدخال النص" + "multi_line_chat_input": "\u0625\u062f\u062e\u0627\u0644 \u062f\u0631\u062f\u0634\u0629 \u0645\u062a\u0639\u062f\u062f \u0627\u0644\u0623\u0633\u0637\u0631", + "bypass_text_input_limit": "\u062a\u062c\u0627\u0648\u0632 \u062d\u062f \u0625\u062f\u062e\u0627\u0644 \u0627\u0644\u0646\u0635" }, "auto_purge": { - "never": "أبداً", - "1_hour": "ساعة واحدة", - "3_hours": "3 ساعات", - "6_hours": "6 ساعات", - "12_hours": "12 ساعة", - "1_day": "يوم واحد", - "3_days": "3 أيام", - "1_week": "أسبوع واحد", - "2_weeks": "أسبوعين", - "1_month": "شهر واحد", - "3_months": "3 أشهر", - "6_months": "6 أشهر" + "never": "\u0623\u0628\u062f\u0627\u064b", + "1_hour": "\u0633\u0627\u0639\u0629 \u0648\u0627\u062d\u062f\u0629", + "3_hours": "3 \u0633\u0627\u0639\u0627\u062a", + "6_hours": "6 \u0633\u0627\u0639\u0627\u062a", + "12_hours": "12 \u0633\u0627\u0639\u0629", + "1_day": "\u064a\u0648\u0645 \u0648\u0627\u062d\u062f", + "3_days": "3 \u0623\u064a\u0627\u0645", + "1_week": "\u0623\u0633\u0628\u0648\u0639 \u0648\u0627\u062d\u062f", + "2_weeks": "\u0623\u0633\u0628\u0648\u0639\u064a\u0646", + "1_month": "\u0634\u0647\u0631 \u0648\u0627\u062d\u062f", + "3_months": "3 \u0623\u0634\u0647\u0631", + "6_months": "6 \u0623\u0634\u0647\u0631" }, "delete_after_unit": { - "seconds": "ثواني", - "minutes": "دقائق", - "hours": "ساعات" + "seconds": "\u062b\u0648\u0627\u0646\u064a", + "minutes": "\u062f\u0642\u0627\u0626\u0642", + "hours": "\u0633\u0627\u0639\u0627\u062a" }, "disable_story_sections": { - "friends": "الأصدقاء", - "suggested_stories": "قصص مقترحة", - "following": "تتابعهم", - "discover": "اكتشف (Discover)" + "friends": "\u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "suggested_stories": "\u0642\u0635\u0635 \u0645\u0642\u062a\u0631\u062d\u0629", + "following": "\u062a\u062a\u0627\u0628\u0639\u0647\u0645", + "discover": "\u0627\u0643\u062a\u0634\u0641 (Discover)" }, "disable_cameras": { - "front": "الكاميرا الأمامية", - "back": "الكاميرا الخلفية" + "front": "\u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u0623\u0645\u0627\u0645\u064a\u0629", + "back": "\u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u062e\u0644\u0641\u064a\u0629" }, "disable_permission_requests": { - "notifications": "الإشعارات", - "read_media_images": "قراءة صور الوسائط", - "read_media_video": "قراءة فيديو الوسائط", - "camera": "الكاميرا", - "microphone": "الميكروفون", - "location": "الموقع", - "read_contacts": "قراءة جهات الاتصال", - "nearby_devices": "الأجهزة المجاورة", - "phone_calls": "المكالمات الهاتفية" + "notifications": "\u0627\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062a", + "read_media_images": "\u0642\u0631\u0627\u0621\u0629 \u0635\u0648\u0631 \u0627\u0644\u0648\u0633\u0627\u0626\u0637", + "read_media_video": "\u0642\u0631\u0627\u0621\u0629 \u0641\u064a\u062f\u064a\u0648 \u0627\u0644\u0648\u0633\u0627\u0626\u0637", + "camera": "\u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627", + "microphone": "\u0627\u0644\u0645\u064a\u0643\u0631\u0648\u0641\u0648\u0646", + "location": "\u0627\u0644\u0645\u0648\u0642\u0639", + "read_contacts": "\u0642\u0631\u0627\u0621\u0629 \u062c\u0647\u0627\u062a \u0627\u0644\u0627\u062a\u0635\u0627\u0644", + "nearby_devices": "\u0627\u0644\u0623\u062c\u0647\u0632\u0629 \u0627\u0644\u0645\u062c\u0627\u0648\u0631\u0629", + "phone_calls": "\u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0627\u062a \u0627\u0644\u0647\u0627\u062a\u0641\u064a\u0629" }, "message_indicators": { - "encryption_indicator": "يضيف أيقونة \ud83d\udd12 بجوار الرسائل التي تم إرسالها إليك فقط", - "platform_indicator": "يضيف أيقونة المنصة التي تم إرسال الوسيط منها (مثل Android، iOS، Web)", - "location_indicator": "يضيف أيقونة \ud83d\udccd للـ snaps عندما يتم إرسالها مع تمكين الموقع", - "ovf_editor_indicator": "يشير إلى ما إذا كان snap قد تم إرساله باستخدام محرر OVF", - "director_mode_indicator": "يضيف أيقونة \u270f\ufe0f للـ snaps عندما يتم إرسالها باستخدام وضع المخرج، والذي يمكن استخدامه لإرسال صور المعرض كـ snaps" + "encryption_indicator": "\u064a\u0636\u064a\u0641 \u0623\u064a\u0642\u0648\u0646\u0629 \ud83d\udd12 \u0628\u062c\u0648\u0627\u0631 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062a\u064a \u062a\u0645 \u0625\u0631\u0633\u0627\u0644\u0647\u0627 \u0625\u0644\u064a\u0643 \u0641\u0642\u0637", + "platform_indicator": "\u064a\u0636\u064a\u0641 \u0623\u064a\u0642\u0648\u0646\u0629 \u0627\u0644\u0645\u0646\u0635\u0629 \u0627\u0644\u062a\u064a \u062a\u0645 \u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0648\u0633\u064a\u0637 \u0645\u0646\u0647\u0627 (\u0645\u062b\u0644 Android\u060c iOS\u060c Web)", + "location_indicator": "\u064a\u0636\u064a\u0641 \u0623\u064a\u0642\u0648\u0646\u0629 \ud83d\udccd \u0644\u0644\u0640 snaps \u0639\u0646\u062f\u0645\u0627 \u064a\u062a\u0645 \u0625\u0631\u0633\u0627\u0644\u0647\u0627 \u0645\u0639 \u062a\u0645\u0643\u064a\u0646 \u0627\u0644\u0645\u0648\u0642\u0639", + "ovf_editor_indicator": "\u064a\u0634\u064a\u0631 \u0625\u0644\u0649 \u0645\u0627 \u0625\u0630\u0627 \u0643\u0627\u0646 snap \u0642\u062f \u062a\u0645 \u0625\u0631\u0633\u0627\u0644\u0647 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0645\u062d\u0631\u0631 OVF", + "director_mode_indicator": "\u064a\u0636\u064a\u0641 \u0623\u064a\u0642\u0648\u0646\u0629 \u270f\ufe0f \u0644\u0644\u0640 snaps \u0639\u0646\u062f\u0645\u0627 \u064a\u062a\u0645 \u0625\u0631\u0633\u0627\u0644\u0647\u0627 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0648\u0636\u0639 \u0627\u0644\u0645\u062e\u0631\u062c\u060c \u0648\u0627\u0644\u0630\u064a \u064a\u0645\u0643\u0646 \u0627\u0633\u062a\u062e\u062f\u0627\u0645\u0647 \u0644\u0625\u0631\u0633\u0627\u0644 \u0635\u0648\u0631 \u0627\u0644\u0645\u0639\u0631\u0636 \u0643\u0640 snaps", + "memories_indicator": "\u064a\u0636\u064a\u0641 \u0623\u064a\u0642\u0648\u0646\u0629 \ud83d\udcd6 \u0625\u0644\u0649 snaps \u0627\u0644\u062a\u064a \u062a\u0645\u062a \u0625\u0639\u0627\u062f\u0629 \u0625\u0631\u0633\u0627\u0644\u0647\u0627 \u0645\u0646 \u0627\u0644\u0630\u0643\u0631\u064a\u0627\u062a \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0627\u0644\u062a\u0642\u0627\u0637\u0647\u0627 \u0628\u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627 \u0627\u0644\u062d\u064a\u0629", + "skip_own_indicators": "\u064a\u062e\u0641\u064a \u0623\u064a\u0642\u0648\u0646\u0627\u062a \u0627\u0644\u0645\u0624\u0634\u0631\u0627\u062a \u0639\u0644\u0649 \u0627\u0644\u0633\u0646\u0627\u0628\u0627\u062a \u0627\u0644\u062a\u064a \u062a\u0631\u0633\u0644\u0647\u0627 \u0628\u0646\u0641\u0633\u0643 (\u0633\u0646\u0627\u0628\u0627\u062a \u0630\u0627\u062a\u064a\u0629) \ud83d\udc64", + "disable_indicators_in_groups": "\u064a\u0639\u0637\u0644 \u062c\u0645\u064a\u0639 \u0623\u064a\u0642\u0648\u0646\u0627\u062a \u0627\u0644\u0645\u0624\u0634\u0631\u0627\u062a \u0641\u064a \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0627\u062a \u0627\u0644\u062c\u0645\u0627\u0639\u064a\u0629 \u0644\u062a\u0642\u0644\u064a\u0644 \u0641\u0648\u0636\u0649 \u0648\u0627\u062c\u0647\u0629 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \ud83d\udc65" }, "auto_mark_as_read": { - "conversation_read": "وضع علامة مقروء على المحادثة عند إرسال رسالة", - "snap_reply": "وضع علامة مقروء على snaps عند الرد عليها", - "save_snap_in_chat": "وضع علامة مقروء على snaps عند حفظها في الدردشة أثناء وضع التخفي" + "conversation_read": "\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0645\u0642\u0631\u0648\u0621 \u0639\u0644\u0649 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629 \u0639\u0646\u062f \u0625\u0631\u0633\u0627\u0644 \u0631\u0633\u0627\u0644\u0629", + "snap_reply": "\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0645\u0642\u0631\u0648\u0621 \u0639\u0644\u0649 snaps \u0639\u0646\u062f \u0627\u0644\u0631\u062f \u0639\u0644\u064a\u0647\u0627", + "save_snap_in_chat": "\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0645\u0642\u0631\u0648\u0621 \u0639\u0644\u0649 snaps \u0639\u0646\u062f \u062d\u0641\u0638\u0647\u0627 \u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629 \u0623\u062b\u0646\u0627\u0621 \u0648\u0636\u0639 \u0627\u0644\u062a\u062e\u0641\u064a" }, "friend_mutation_notifier": { - "remove_friend": "إشعار عندما يزيلك شخص ما كصديق", - "birthday_changes": "إشعار عندما يغير شخص ما عيد ميلاده", - "bitmoji_selfie_changes": "إشعار عندما يغير شخص ما سيلفي Bitmoji الخاص به", - "bitmoji_avatar_changes": "إشعار عندما يغير شخص ما أفاتار Bitmoji الخاص به", - "bitmoji_background_changes": "إشعار عندما يغير شخص ما خلفية Bitmoji الخاصة به", - "bitmoji_scene_changes": "إشعار عندما يغير شخص ما مشهد Bitmoji الخاص به" + "remove_friend": "\u0625\u0634\u0639\u0627\u0631 \u0639\u0646\u062f\u0645\u0627 \u064a\u0632\u064a\u0644\u0643 \u0634\u062e\u0635 \u0645\u0627 \u0643\u0635\u062f\u064a\u0642", + "birthday_changes": "\u0625\u0634\u0639\u0627\u0631 \u0639\u0646\u062f\u0645\u0627 \u064a\u063a\u064a\u0631 \u0634\u062e\u0635 \u0645\u0627 \u0639\u064a\u062f \u0645\u064a\u0644\u0627\u062f\u0647", + "bitmoji_selfie_changes": "\u0625\u0634\u0639\u0627\u0631 \u0639\u0646\u062f\u0645\u0627 \u064a\u063a\u064a\u0631 \u0634\u062e\u0635 \u0645\u0627 \u0633\u064a\u0644\u0641\u064a Bitmoji \u0627\u0644\u062e\u0627\u0635 \u0628\u0647", + "bitmoji_avatar_changes": "\u0625\u0634\u0639\u0627\u0631 \u0639\u0646\u062f\u0645\u0627 \u064a\u063a\u064a\u0631 \u0634\u062e\u0635 \u0645\u0627 \u0623\u0641\u0627\u062a\u0627\u0631 Bitmoji \u0627\u0644\u062e\u0627\u0635 \u0628\u0647", + "bitmoji_background_changes": "\u0625\u0634\u0639\u0627\u0631 \u0639\u0646\u062f\u0645\u0627 \u064a\u063a\u064a\u0631 \u0634\u062e\u0635 \u0645\u0627 \u062e\u0644\u0641\u064a\u0629 Bitmoji \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0647", + "bitmoji_scene_changes": "\u0625\u0634\u0639\u0627\u0631 \u0639\u0646\u062f\u0645\u0627 \u064a\u063a\u064a\u0631 \u0634\u062e\u0635 \u0645\u0627 \u0645\u0634\u0647\u062f Bitmoji \u0627\u0644\u062e\u0627\u0635 \u0628\u0647" }, "double_tap_chat_action": { - "like_message": "إعجاب بالرسالة", - "copy_text": "نسخ النص إلى الحافظة", - "delete_message": "حذف الرسالة", - "mark_as_read": "وضع علامة مقروء", - "custom_emoji_reaction": "تفاعل إيموجي مخصص", - "null": "افتراضي" + "like_message": "\u0625\u0639\u062c\u0627\u0628 \u0628\u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "copy_text": "\u0646\u0633\u062e \u0627\u0644\u0646\u0635 \u0625\u0644\u0649 \u0627\u0644\u062d\u0627\u0641\u0638\u0629", + "delete_message": "\u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "mark_as_read": "\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0645\u0642\u0631\u0648\u0621", + "custom_emoji_reaction": "\u062a\u0641\u0627\u0639\u0644 \u0625\u064a\u0645\u0648\u062c\u064a \u0645\u062e\u0635\u0635", + "null": "\u0627\u0641\u062a\u0631\u0627\u0636\u064a" }, "message_types": { - "CHAT": "دردشة", + "CHAT": "\u062f\u0631\u062f\u0634\u0629", "SNAP": "Snap", - "NOTE": "ملاحظة", - "EXTERNAL_MEDIA": "وسائط خارجية", - "STICKER": "ملصق" + "NOTE": "\u0645\u0644\u0627\u062d\u0638\u0629", + "EXTERNAL_MEDIA": "\u0648\u0633\u0627\u0626\u0637 \u062e\u0627\u0631\u062c\u064a\u0629", + "STICKER": "\u0645\u0644\u0635\u0642" }, "double_tap_chat_action_custom_emoji": { - "Custom emoji reaction": "تفاعل إيموجي مخصص" + "Custom emoji reaction": "\u062a\u0641\u0627\u0639\u0644 \u0625\u064a\u0645\u0648\u062c\u064a \u0645\u062e\u0635\u0635" }, "ai_model": { "gemini-2.5-flash": "Gemini 2.5 Flash" }, "ai_api_key": { - "": "غير معين" + "": "\u063a\u064a\u0631 \u0645\u0639\u064a\u0646" }, "ai_system_prompt": { - "You are a helpful and friendly assistant responding to messages on Snapchat. Keep responses natural, casual, and conversational. Avoid being overly formal or robotic. Respond as if you're a real person having a normal conversation.": "أنت مساعد مفيد وودود ترد على الرسائل في Snapchat. اجعل الردود طبيعية وغير رسمية ومحادثة. تجنب أن تكون رسمياً جداً أو آلياً. رد كما لو كنت شخصاً حقيقياً يجري محادثة طبيعية." + "You are a helpful and friendly assistant responding to messages on Snapchat. Keep responses natural, casual, and conversational. Avoid being overly formal or robotic. Respond as if you're a real person having a normal conversation.": "\u0623\u0646\u062a \u0645\u0633\u0627\u0639\u062f \u0645\u0641\u064a\u062f \u0648\u0648\u062f\u0648\u062f \u062a\u0631\u062f \u0639\u0644\u0649 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0641\u064a Snapchat. \u0627\u062c\u0639\u0644 \u0627\u0644\u0631\u062f\u0648\u062f \u0637\u0628\u064a\u0639\u064a\u0629 \u0648\u063a\u064a\u0631 \u0631\u0633\u0645\u064a\u0629 \u0648\u0645\u062d\u0627\u062f\u062b\u0629. \u062a\u062c\u0646\u0628 \u0623\u0646 \u062a\u0643\u0648\u0646 \u0631\u0633\u0645\u064a\u0627\u064b \u062c\u062f\u0627\u064b \u0623\u0648 \u0622\u0644\u064a\u0627\u064b. \u0631\u062f \u0643\u0645\u0627 \u0644\u0648 \u0643\u0646\u062a \u0634\u062e\u0635\u0627\u064b \u062d\u0642\u064a\u0642\u064a\u0627\u064b \u064a\u062c\u0631\u064a \u0645\u062d\u0627\u062f\u062b\u0629 \u0637\u0628\u064a\u0639\u064a\u0629." }, "ai_provider": { "gemini": "Gemini", @@ -2619,1053 +2635,1053 @@ "openrouter": "OpenRouter" }, "ai_personality_traits": { - "friendly, casual, helpful, empathetic": "ودود، عفوي، مفيد، متعاطف" + "friendly, casual, helpful, empathetic": "\u0648\u062f\u0648\u062f\u060c \u0639\u0641\u0648\u064a\u060c \u0645\u0641\u064a\u062f\u060c \u0645\u062a\u0639\u0627\u0637\u0641" }, "ai_response_style": { - "casual": "عفوي", - "formal": "رسمي", - "friendly": "ودود", - "humorous": "فكاهي", - "empathetic": "متعاطف", - "toxic": "حاد الطباع", - "busy": "مشغول" + "casual": "\u0639\u0641\u0648\u064a", + "formal": "\u0631\u0633\u0645\u064a", + "friendly": "\u0648\u062f\u0648\u062f", + "humorous": "\u0641\u0643\u0627\u0647\u064a", + "empathetic": "\u0645\u062a\u0639\u0627\u0637\u0641", + "toxic": "\u062d\u0627\u062f \u0627\u0644\u0637\u0628\u0627\u0639", + "busy": "\u0645\u0634\u063a\u0648\u0644" }, "ai_temperature": { - "0.7": "متوازن (0.7)" + "0.7": "\u0645\u062a\u0648\u0627\u0632\u0646 (0.7)" }, "ai_response_language": { - "auto": "تلقائي", - "en": "الإنجليزية", - "es": "الإسبانية", - "fr": "الفرنسية", - "de": "الألمانية", - "it": "الإيطالية", - "pt": "البرتغالية", - "ru": "الروسية", - "ja": "اليابانية", - "ko": "الكورية", - "zh": "الصينية", - "ar": "العربية", - "hi": "الهندية", - "tr": "التركية", - "pl": "البولندية", - "nl": "الهولندية", - "sv": "السويدية", - "da": "الدانماركية", - "no": "النرويجية", - "fi": "الفنلندية" + "auto": "\u062a\u0644\u0642\u0627\u0626\u064a", + "en": "\u0627\u0644\u0625\u0646\u062c\u0644\u064a\u0632\u064a\u0629", + "es": "\u0627\u0644\u0625\u0633\u0628\u0627\u0646\u064a\u0629", + "fr": "\u0627\u0644\u0641\u0631\u0646\u0633\u064a\u0629", + "de": "\u0627\u0644\u0623\u0644\u0645\u0627\u0646\u064a\u0629", + "it": "\u0627\u0644\u0625\u064a\u0637\u0627\u0644\u064a\u0629", + "pt": "\u0627\u0644\u0628\u0631\u062a\u063a\u0627\u0644\u064a\u0629", + "ru": "\u0627\u0644\u0631\u0648\u0633\u064a\u0629", + "ja": "\u0627\u0644\u064a\u0627\u0628\u0627\u0646\u064a\u0629", + "ko": "\u0627\u0644\u0643\u0648\u0631\u064a\u0629", + "zh": "\u0627\u0644\u0635\u064a\u0646\u064a\u0629", + "ar": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", + "hi": "\u0627\u0644\u0647\u0646\u062f\u064a\u0629", + "tr": "\u0627\u0644\u062a\u0631\u0643\u064a\u0629", + "pl": "\u0627\u0644\u0628\u0648\u0644\u0646\u062f\u064a\u0629", + "nl": "\u0627\u0644\u0647\u0648\u0644\u0646\u062f\u064a\u0629", + "sv": "\u0627\u0644\u0633\u0648\u064a\u062f\u064a\u0629", + "da": "\u0627\u0644\u062f\u0627\u0646\u0645\u0627\u0631\u0643\u064a\u0629", + "no": "\u0627\u0644\u0646\u0631\u0648\u064a\u062c\u064a\u0629", + "fi": "\u0627\u0644\u0641\u0646\u0644\u0646\u062f\u064a\u0629" }, "friendGreeting": { - "Hey": "هلا" + "Hey": "\u0647\u0644\u0627" }, "half_swipe_messages": { - "[\"I noticed you half-swiped! I'll respond soon.\"]": "لاحظت أنك قمت بالتمرير النصفي! سأرد قريباً." + "[\"I noticed you half-swiped! I'll respond soon.\"]": "\u0644\u0627\u062d\u0638\u062a \u0623\u0646\u0643 \u0642\u0645\u062a \u0628\u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a! \u0633\u0623\u0631\u062f \u0642\u0631\u064a\u0628\u0627\u064b." }, "tiny_snap_messages": { - "Thanks for the tiny snap!": "شكراً على الـ tiny snap!" + "Thanks for the tiny snap!": "\u0634\u0643\u0631\u0627\u064b \u0639\u0644\u0649 \u0627\u0644\u0640 tiny snap!" }, "voice_note_messages": { - "Thanks for the voice note!": "شكراً على الملاحظة الصوتية!" + "Thanks for the voice note!": "\u0634\u0643\u0631\u0627\u064b \u0639\u0644\u0649 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0629 \u0627\u0644\u0635\u0648\u062a\u064a\u0629!" }, "chat_messages": { - "Hello! How are you?": "أهلاً! كيف حالك؟" + "Hello! How are you?": "\u0623\u0647\u0644\u0627\u064b! \u0643\u064a\u0641 \u062d\u0627\u0644\u0643\u061f" }, "story_reply_messages": { - "Thanks for the story reply!": "شكراً على الرد على القصة!" + "Thanks for the story reply!": "\u0634\u0643\u0631\u0627\u064b \u0639\u0644\u0649 \u0627\u0644\u0631\u062f \u0639\u0644\u0649 \u0627\u0644\u0642\u0635\u0629!" }, "external_media_messages": { - "Nice media!": "وسائط رائعة!" + "Nice media!": "\u0648\u0633\u0627\u0626\u0637 \u0631\u0627\u0626\u0639\u0629!" }, "sticker_messages": { - "Cool sticker!": "ملصق رائع!" + "Cool sticker!": "\u0645\u0644\u0635\u0642 \u0631\u0627\u0626\u0639!" }, "snap_messages": { - "Thanks for the snap!": "شكراً على الـ snap!" + "Thanks for the snap!": "\u0634\u0643\u0631\u0627\u064b \u0639\u0644\u0649 \u0627\u0644\u0640 snap!" }, "story_share_messages": { - "Thanks for sharing!": "شكراً على المشاركة!" + "Thanks for sharing!": "\u0634\u0643\u0631\u0627\u064b \u0639\u0644\u0649 \u0627\u0644\u0645\u0634\u0627\u0631\u0643\u0629!" }, "map_reaction_messages": { - "Thanks for the map reaction!": "شكراً على تفاعل الخريطة!" + "Thanks for the map reaction!": "\u0634\u0643\u0631\u0627\u064b \u0639\u0644\u0649 \u062a\u0641\u0627\u0639\u0644 \u0627\u0644\u062e\u0631\u064a\u0637\u0629!" }, "auto_reply_content_types": { - "chat_messages": "رسائل الدردشة", + "chat_messages": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062f\u0631\u062f\u0634\u0629", "snap_messages": "Snaps", - "story_share_messages": "مشاركات القصة", - "story_reply_messages": "ردود القصة", - "external_media_messages": "الوسائط الخارجية", - "voice_note_messages": "الملاحظات الصوتية", - "sticker_messages": "الملصقات", + "story_share_messages": "\u0645\u0634\u0627\u0631\u0643\u0627\u062a \u0627\u0644\u0642\u0635\u0629", + "story_reply_messages": "\u0631\u062f\u0648\u062f \u0627\u0644\u0642\u0635\u0629", + "external_media_messages": "\u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u062e\u0627\u0631\u062c\u064a\u0629", + "voice_note_messages": "\u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0635\u0648\u062a\u064a\u0629", + "sticker_messages": "\u0627\u0644\u0645\u0644\u0635\u0642\u0627\u062a", "tiny_snap_messages": "Tiny Snaps", - "map_reaction_messages": "تفاعلات الخريطة", - "half_swipes": "التمرير النصفي" + "map_reaction_messages": "\u062a\u0641\u0627\u0639\u0644\u0627\u062a \u0627\u0644\u062e\u0631\u064a\u0637\u0629", + "half_swipes": "\u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a" }, "supported_languages": { - "en": "الإنجليزية", - "es": "الإسبانية", - "fr": "الفرنسية", - "de": "الألمانية", - "it": "الإيطالية", - "pt": "البرتغالية", - "ru": "الروسية", - "ja": "اليابانية", - "ko": "الكورية", - "zh": "الصينية", - "ar": "العربية", - "hi": "الهندية", - "tr": "التركية" + "en": "\u0627\u0644\u0625\u0646\u062c\u0644\u064a\u0632\u064a\u0629", + "es": "\u0627\u0644\u0625\u0633\u0628\u0627\u0646\u064a\u0629", + "fr": "\u0627\u0644\u0641\u0631\u0646\u0633\u064a\u0629", + "de": "\u0627\u0644\u0623\u0644\u0645\u0627\u0646\u064a\u0629", + "it": "\u0627\u0644\u0625\u064a\u0637\u0627\u0644\u064a\u0629", + "pt": "\u0627\u0644\u0628\u0631\u062a\u063a\u0627\u0644\u064a\u0629", + "ru": "\u0627\u0644\u0631\u0648\u0633\u064a\u0629", + "ja": "\u0627\u0644\u064a\u0627\u0628\u0627\u0646\u064a\u0629", + "ko": "\u0627\u0644\u0643\u0648\u0631\u064a\u0629", + "zh": "\u0627\u0644\u0635\u064a\u0646\u064a\u0629", + "ar": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", + "hi": "\u0627\u0644\u0647\u0646\u062f\u064a\u0629", + "tr": "\u0627\u0644\u062a\u0631\u0643\u064a\u0629" }, "translation_position": { - "above": "فوق النص", - "below": "تحت النص", - "inline": "ضمن السطر" + "above": "\u0641\u0648\u0642 \u0627\u0644\u0646\u0635", + "below": "\u062a\u062d\u062a \u0627\u0644\u0646\u0635", + "inline": "\u0636\u0645\u0646 \u0627\u0644\u0633\u0637\u0631" }, "source_language": { - "auto": "كشف تلقائي" + "auto": "\u0643\u0634\u0641 \u062a\u0644\u0642\u0627\u0626\u064a" }, "target_language": { - "en": "الإنجليزية" + "en": "\u0627\u0644\u0625\u0646\u062c\u0644\u064a\u0632\u064a\u0629" } }, "friend_notes": { - "placeholder": "إضافة ملاحظة..." + "placeholder": "\u0625\u0636\u0627\u0641\u0629 \u0645\u0644\u0627\u062d\u0638\u0629..." } }, "friend_menu_option": { - "mark_snaps_as_seen": "وضع علامة \"تمت المشاهدة\" على Snaps", - "mark_stories_as_seen_locally": "وضع علامة \"تمت المشاهدة\" على القصص محلياً", - "preview": "معاينة", - "stealth_mode": "وضع التخفي", - "auto_download_blacklist": "القائمة السوداء للتنزيل التلقائي", - "anti_auto_save": "ضد الحفظ التلقائي" + "mark_snaps_as_seen": "\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\" \u0639\u0644\u0649 Snaps", + "mark_stories_as_seen_locally": "\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\" \u0639\u0644\u0649 \u0627\u0644\u0642\u0635\u0635 \u0645\u062d\u0644\u064a\u0627\u064b", + "preview": "\u0645\u0639\u0627\u064a\u0646\u0629", + "stealth_mode": "\u0648\u0636\u0639 \u0627\u0644\u062a\u062e\u0641\u064a", + "auto_download_blacklist": "\u0627\u0644\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0633\u0648\u062f\u0627\u0621 \u0644\u0644\u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "anti_auto_save": "\u0636\u062f \u0627\u0644\u062d\u0641\u0638 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a" }, "content_type": { - "CHAT": "دردشة", + "CHAT": "\u062f\u0631\u062f\u0634\u0629", "SNAP": "Snap", - "EXTERNAL_MEDIA": "وسائط خارجية", - "NOTE": "ملاحظة صوتية", - "STICKER": "ملصق", - "SHARE": "مشاركة", - "STATUS": "الحالة", - "LOCATION": "الموقع", - "STATUS_SAVE_TO_CAMERA_ROLL": "تم الحفظ في ألبوم الكاميرا", - "STATUS_CONVERSATION_CAPTURE_SCREENSHOT": "لقطة شاشة", - "STATUS_CONVERSATION_CAPTURE_RECORD": "تسجيل الشاشة", - "STATUS_CALL_MISSED_VIDEO": "مكالمة فيديو فائتة", - "STATUS_CALL_MISSED_AUDIO": "مكالمة صوتية فائتة", - "LIVE_LOCATION_SHARE": "مشاركة الموقع المباشر", - "CREATIVE_TOOL_ITEM": "عنصر أداة إبداعية", - "FAMILY_CENTER_INVITE": "دعوة مركز العائلة", - "FAMILY_CENTER_ACCEPT": "قبول مركز العائلة", - "FAMILY_CENTER_LEAVE": "مغادرة مركز العائلة", - "STATUS_PLUS_GIFT": "هدية Status Plus", + "EXTERNAL_MEDIA": "\u0648\u0633\u0627\u0626\u0637 \u062e\u0627\u0631\u062c\u064a\u0629", + "NOTE": "\u0645\u0644\u0627\u062d\u0638\u0629 \u0635\u0648\u062a\u064a\u0629", + "STICKER": "\u0645\u0644\u0635\u0642", + "SHARE": "\u0645\u0634\u0627\u0631\u0643\u0629", + "STATUS": "\u0627\u0644\u062d\u0627\u0644\u0629", + "LOCATION": "\u0627\u0644\u0645\u0648\u0642\u0639", + "STATUS_SAVE_TO_CAMERA_ROLL": "\u062a\u0645 \u0627\u0644\u062d\u0641\u0638 \u0641\u064a \u0623\u0644\u0628\u0648\u0645 \u0627\u0644\u0643\u0627\u0645\u064a\u0631\u0627", + "STATUS_CONVERSATION_CAPTURE_SCREENSHOT": "\u0644\u0642\u0637\u0629 \u0634\u0627\u0634\u0629", + "STATUS_CONVERSATION_CAPTURE_RECORD": "\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u0634\u0627\u0634\u0629", + "STATUS_CALL_MISSED_VIDEO": "\u0645\u0643\u0627\u0644\u0645\u0629 \u0641\u064a\u062f\u064a\u0648 \u0641\u0627\u0626\u062a\u0629", + "STATUS_CALL_MISSED_AUDIO": "\u0645\u0643\u0627\u0644\u0645\u0629 \u0635\u0648\u062a\u064a\u0629 \u0641\u0627\u0626\u062a\u0629", + "LIVE_LOCATION_SHARE": "\u0645\u0634\u0627\u0631\u0643\u0629 \u0627\u0644\u0645\u0648\u0642\u0639 \u0627\u0644\u0645\u0628\u0627\u0634\u0631", + "CREATIVE_TOOL_ITEM": "\u0639\u0646\u0635\u0631 \u0623\u062f\u0627\u0629 \u0625\u0628\u062f\u0627\u0639\u064a\u0629", + "FAMILY_CENTER_INVITE": "\u062f\u0639\u0648\u0629 \u0645\u0631\u0643\u0632 \u0627\u0644\u0639\u0627\u0626\u0644\u0629", + "FAMILY_CENTER_ACCEPT": "\u0642\u0628\u0648\u0644 \u0645\u0631\u0643\u0632 \u0627\u0644\u0639\u0627\u0626\u0644\u0629", + "FAMILY_CENTER_LEAVE": "\u0645\u063a\u0627\u062f\u0631\u0629 \u0645\u0631\u0643\u0632 \u0627\u0644\u0639\u0627\u0626\u0644\u0629", + "STATUS_PLUS_GIFT": "\u0647\u062f\u064a\u0629 Status Plus", "TINY_SNAP": "Tiny Snap", - "STATUS_COUNTDOWN": "عد تنازلي", - "MAP_REACTION": "تفاعل الخريطة", - "chat_messages": "رسائل الدردشة", + "STATUS_COUNTDOWN": "\u0639\u062f \u062a\u0646\u0627\u0632\u0644\u064a", + "MAP_REACTION": "\u062a\u0641\u0627\u0639\u0644 \u0627\u0644\u062e\u0631\u064a\u0637\u0629", + "chat_messages": "\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u062f\u0631\u062f\u0634\u0629", "snap_messages": "Snaps", - "story_share_messages": "مشاركات القصة", - "story_reply_messages": "ردود القصة", - "external_media_messages": "الوسائط الخارجية", - "voice_note_messages": "ملاحظة صوتية", - "sticker_messages": "ملصق", + "story_share_messages": "\u0645\u0634\u0627\u0631\u0643\u0627\u062a \u0627\u0644\u0642\u0635\u0629", + "story_reply_messages": "\u0631\u062f\u0648\u062f \u0627\u0644\u0642\u0635\u0629", + "external_media_messages": "\u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u062e\u0627\u0631\u062c\u064a\u0629", + "voice_note_messages": "\u0645\u0644\u0627\u062d\u0638\u0629 \u0635\u0648\u062a\u064a\u0629", + "sticker_messages": "\u0645\u0644\u0635\u0642", "tiny_snap_messages": "Tiny Snap", - "map_reaction_messages": "تفاعل الخريطة", - "half_swipes": "التمرير النصفي" + "map_reaction_messages": "\u062a\u0641\u0627\u0639\u0644 \u0627\u0644\u062e\u0631\u064a\u0637\u0629", + "half_swipes": "\u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a" }, "media_download_source": { - "none": "لا شيء", - "pending": "قيد الانتظار", - "chat_media": "وسائط الدردشة", - "story": "قصة", - "public_story": "قصة عامة", + "none": "\u0644\u0627 \u0634\u064a\u0621", + "pending": "\u0642\u064a\u062f \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631", + "chat_media": "\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "story": "\u0642\u0635\u0629", + "public_story": "\u0642\u0635\u0629 \u0639\u0627\u0645\u0629", "spotlight": "Spotlight", - "profile_picture": "صورة الملف الشخصي", - "story_logger": "مسجل القصة", - "message_logger": "مسجل الرسائل", - "merged": "مدمج", - "voice_call": "مكالمة صوتية", - "chat_wallpaper": "خلفية الدردشة" + "profile_picture": "\u0635\u0648\u0631\u0629 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a", + "story_logger": "\u0645\u0633\u062c\u0644 \u0627\u0644\u0642\u0635\u0629", + "message_logger": "\u0645\u0633\u062c\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "merged": "\u0645\u062f\u0645\u062c", + "voice_call": "\u0645\u0643\u0627\u0644\u0645\u0629 \u0635\u0648\u062a\u064a\u0629", + "chat_wallpaper": "\u062e\u0644\u0641\u064a\u0629 \u0627\u0644\u062f\u0631\u062f\u0634\u0629" }, "chat_action_menu": { - "preview_button": "معاينة", - "download_button": "تنزيل", - "delete_logged_message_button": "حذف الرسالة المسجلة", - "show_chat_edit_history": "عرض سجل تحرير الدردشة", - "convert_message": "تحويل الرسالة" + "preview_button": "\u0645\u0639\u0627\u064a\u0646\u0629", + "download_button": "\u062a\u0646\u0632\u064a\u0644", + "delete_logged_message_button": "\u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u0627\u0644\u0645\u0633\u062c\u0644\u0629", + "show_chat_edit_history": "\u0639\u0631\u0636 \u0633\u062c\u0644 \u062a\u062d\u0631\u064a\u0631 \u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "convert_message": "\u062a\u062d\u0648\u064a\u0644 \u0627\u0644\u0631\u0633\u0627\u0644\u0629" }, "chat_wallpaper_downloader": { - "download_button": "تنزيل خلفية الدردشة" + "download_button": "\u062a\u0646\u0632\u064a\u0644 \u062e\u0644\u0641\u064a\u0629 \u0627\u0644\u062f\u0631\u062f\u0634\u0629" }, "opera_context_menu": { - "download": "تنزيل الوسائط", - "sent_at": "أرسلت في {date}", - "created_at": "أنشئت في {date}", - "expires_at": "تنتهي في {date}", - "media_size": "حجم الوسائط: {size}", - "media_duration": "مدة الوسائط: {duration} مللي ثانية", - "show_debug_info": "عرض معلومات التصحيح" + "download": "\u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637", + "sent_at": "\u0623\u0631\u0633\u0644\u062a \u0641\u064a {date}", + "created_at": "\u0623\u0646\u0634\u0626\u062a \u0641\u064a {date}", + "expires_at": "\u062a\u0646\u062a\u0647\u064a \u0641\u064a {date}", + "media_size": "\u062d\u062c\u0645 \u0627\u0644\u0648\u0633\u0627\u0626\u0637: {size}", + "media_duration": "\u0645\u062f\u0629 \u0627\u0644\u0648\u0633\u0627\u0626\u0637: {duration} \u0645\u0644\u0644\u064a \u062b\u0627\u0646\u064a\u0629", + "show_debug_info": "\u0639\u0631\u0636 \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u062a\u0635\u062d\u064a\u062d" }, "modal_option": { - "profile_info": "معلومات الملف الشخصي", - "close": "إغلاق" + "profile_info": "\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a", + "close": "\u0625\u063a\u0644\u0627\u0642" }, "gallery_media_send_override": { - "always_ask": "اسأل دائماً", - "ORIGINAL": "الوسائط الأصلية", - "NOTE": "ملاحظة صوتية", + "always_ask": "\u0627\u0633\u0623\u0644 \u062f\u0627\u0626\u0645\u0627\u064b", + "ORIGINAL": "\u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u0623\u0635\u0644\u064a\u0629", + "NOTE": "\u0645\u0644\u0627\u062d\u0638\u0629 \u0635\u0648\u062a\u064a\u0629", "SNAP": "Snap", - "SAVEABLE_SNAP": "Snap قابل للحفظ", - "null": "افتراضي Snapchat", - "multiple_media_toast": "يمكنك إرسال وسيط واحد فقط في المرة الواحدة" + "SAVEABLE_SNAP": "Snap \u0642\u0627\u0628\u0644 \u0644\u0644\u062d\u0641\u0638", + "null": "\u0627\u0641\u062a\u0631\u0627\u0636\u064a Snapchat", + "multiple_media_toast": "\u064a\u0645\u0643\u0646\u0643 \u0625\u0631\u0633\u0627\u0644 \u0648\u0633\u064a\u0637 \u0648\u0627\u062d\u062f \u0641\u0642\u0637 \u0641\u064a \u0627\u0644\u0645\u0631\u0629 \u0627\u0644\u0648\u0627\u062d\u062f\u0629" }, "mark_as_seen": { - "no_unseen_snaps_toast": "لم يتم العثور على Snaps غير مرئية!", - "seen_toast": "تم وضع علامة \"تمت المشاهدة\"!", - "unseen_toast": "تم وضع علامة \"غير مرئي\"!", - "already_seen_toast": "تم وضع علامة \"تمت المشاهدة\" بالفعل!", - "already_unseen_toast": "تم وضع علامة \"غير مرئي\" بالفعل!" + "no_unseen_snaps_toast": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 Snaps \u063a\u064a\u0631 \u0645\u0631\u0626\u064a\u0629!", + "seen_toast": "\u062a\u0645 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\"!", + "unseen_toast": "\u062a\u0645 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u063a\u064a\u0631 \u0645\u0631\u0626\u064a\"!", + "already_seen_toast": "\u062a\u0645 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u062a\u0645\u062a \u0627\u0644\u0645\u0634\u0627\u0647\u062f\u0629\" \u0628\u0627\u0644\u0641\u0639\u0644!", + "already_unseen_toast": "\u062a\u0645 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \"\u063a\u064a\u0631 \u0645\u0631\u0626\u064a\" \u0628\u0627\u0644\u0641\u0639\u0644!" }, "conversation_preview": { - "streak_expiration": "ينتهي خلال {day} أيام و {hour} ساعات و {minute} دقيقة", - "total_messages": "إجمالي الرسائل المرسلة/المستلمة: \n{count}", - "title": "معاينة", - "unknown_user": "مستخدم غير معروف", - "no_messages": "لم يتم العثور على رسائل!" + "streak_expiration": "\u064a\u0646\u062a\u0647\u064a \u062e\u0644\u0627\u0644 {day} \u0623\u064a\u0627\u0645 \u0648 {hour} \u0633\u0627\u0639\u0627\u062a \u0648 {minute} \u062f\u0642\u064a\u0642\u0629", + "total_messages": "\u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0631\u0633\u0644\u0629/\u0627\u0644\u0645\u0633\u062a\u0644\u0645\u0629: \n{count}", + "title": "\u0645\u0639\u0627\u064a\u0646\u0629", + "unknown_user": "\u0645\u0633\u062a\u062e\u062f\u0645 \u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641", + "no_messages": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0631\u0633\u0627\u0626\u0644!" }, "profile_info": { - "title": "معلومات الملف الشخصي", - "first_created_username": "اسم المستخدم الذي تم إنشاؤه أولاً", - "mutable_username": "اسم المستخدم القابل للتغيير", - "display_name": "اسم العرض", - "added_date": "تاريخ الإضافة", - "birthday": "عيد الميلاد : {month} {day}", - "hidden_birthday": "عيد الميلاد : مخفي", - "friendship": "الصداقة", - "add_source": "مصدر الإضافة", + "title": "\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a", + "first_created_username": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u0630\u064a \u062a\u0645 \u0625\u0646\u0634\u0627\u0624\u0647 \u0623\u0648\u0644\u0627\u064b", + "mutable_username": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u0642\u0627\u0628\u0644 \u0644\u0644\u062a\u063a\u064a\u064a\u0631", + "display_name": "\u0627\u0633\u0645 \u0627\u0644\u0639\u0631\u0636", + "added_date": "\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0625\u0636\u0627\u0641\u0629", + "birthday": "\u0639\u064a\u062f \u0627\u0644\u0645\u064a\u0644\u0627\u062f : {month} {day}", + "hidden_birthday": "\u0639\u064a\u062f \u0627\u0644\u0645\u064a\u0644\u0627\u062f : \u0645\u062e\u0641\u064a", + "friendship": "\u0627\u0644\u0635\u062f\u0627\u0642\u0629", + "add_source": "\u0645\u0635\u062f\u0631 \u0627\u0644\u0625\u0636\u0627\u0641\u0629", "snapchat_plus": "Snapchat Plus", "snapchat_plus_state": { - "subscribed": "مشترك", - "not_subscribed": "غير مشترك" + "subscribed": "\u0645\u0634\u062a\u0631\u0643", + "not_subscribed": "\u063a\u064a\u0631 \u0645\u0634\u062a\u0631\u0643" } }, "snapchat_plus_state": { - "subscribed": "مشترك", - "not_subscribed": "غير مشترك" + "subscribed": "\u0645\u0634\u062a\u0631\u0643", + "not_subscribed": "\u063a\u064a\u0631 \u0645\u0634\u062a\u0631\u0643" }, "friendship_link_type": { - "mutual": "متبادل", - "outgoing": "صادر", - "blocked": "محظور", - "deleted": "محذوف", - "following": "يتابع", - "suggested": "مقترح", - "incoming": "وارد", - "incoming_follower": "متابع وارد" + "mutual": "\u0645\u062a\u0628\u0627\u062f\u0644", + "outgoing": "\u0635\u0627\u062f\u0631", + "blocked": "\u0645\u062d\u0638\u0648\u0631", + "deleted": "\u0645\u062d\u0630\u0648\u0641", + "following": "\u064a\u062a\u0627\u0628\u0639", + "suggested": "\u0645\u0642\u062a\u0631\u062d", + "incoming": "\u0648\u0627\u0631\u062f", + "incoming_follower": "\u0645\u062a\u0627\u0628\u0639 \u0648\u0627\u0631\u062f" }, "bulk_messaging_action": { - "actions.title": "الإجراءات", - "choose_action_title": "اختر إجراءً", - "progress_status": "معالجة {index} من {total}", - "selection_dialog_continue_button": "متابعة", + "actions.title": "\u0627\u0644\u0625\u062c\u0631\u0627\u0621\u0627\u062a", + "choose_action_title": "\u0627\u062e\u062a\u0631 \u0625\u062c\u0631\u0627\u0621\u064b", + "progress_status": "\u0645\u0639\u0627\u0644\u062c\u0629 {index} \u0645\u0646 {total}", + "selection_dialog_continue_button": "\u0645\u062a\u0627\u0628\u0639\u0629", "confirmation_dialog": { - "title": "هل أنت متأكد؟", - "message": "سيؤثر هذا على جميع المحدد ، هذا الإجراء لا يمكن التراجع عنه." + "title": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f\u061f", + "message": "\u0633\u064a\u0624\u062b\u0631 \u0647\u0630\u0627 \u0639\u0644\u0649 \u062c\u0645\u064a\u0639 \u0627\u0644\u0645\u062d\u062f\u062f \u060c \u0647\u0630\u0627 \u0627\u0644\u0625\u062c\u0631\u0627\u0621 \u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u062a\u0631\u0627\u062c\u0639 \u0639\u0646\u0647." }, "actions": { - "remove_friends": "إزالة الأصدقاء", - "clear_conversations": "مسح المحادثات", - "clear_friend_feed": "مسح موجز الأصدقاء ({count})", - "unfollow": "إلغاء المتابعة", - "remove": "إزالة", - "accept": "قبول", - "ignore": "تجاهل" + "remove_friends": "\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "clear_conversations": "\u0645\u0633\u062d \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0627\u062a", + "clear_friend_feed": "\u0645\u0633\u062d \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 ({count})", + "unfollow": "\u0625\u0644\u063a\u0627\u0621 \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629", + "remove": "\u0625\u0632\u0627\u0644\u0629", + "accept": "\u0642\u0628\u0648\u0644", + "ignore": "\u062a\u062c\u0627\u0647\u0644" }, - "accept_requests": "قبول الطلبات", - "ignore_requests": "تجاهل الطلبات", - "cleared_from_feed": "تم المسح من الموجز", - "leave_groups": "مغادرة {count} مجموعات", - "left_group_success": "تمت مغادرة المجموعة بنجاح", - "failed_to_leave_group": "فشل مغادرة المجموعة: {error}", + "accept_requests": "\u0642\u0628\u0648\u0644 \u0627\u0644\u0637\u0644\u0628\u0627\u062a", + "ignore_requests": "\u062a\u062c\u0627\u0647\u0644 \u0627\u0644\u0637\u0644\u0628\u0627\u062a", + "cleared_from_feed": "\u062a\u0645 \u0627\u0644\u0645\u0633\u062d \u0645\u0646 \u0627\u0644\u0645\u0648\u062c\u0632", + "leave_groups": "\u0645\u063a\u0627\u062f\u0631\u0629 {count} \u0645\u062c\u0645\u0648\u0639\u0627\u062a", + "left_group_success": "\u062a\u0645\u062a \u0645\u063a\u0627\u062f\u0631\u0629 \u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0629 \u0628\u0646\u062c\u0627\u062d", + "failed_to_leave_group": "\u0641\u0634\u0644 \u0645\u063a\u0627\u062f\u0631\u0629 \u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0629: {error}", "conversation_types": { - "friends_only": "الأصدقاء فقط", - "groups_only": "المجموعات فقط", - "both": "الأصدقاء والمجموعات" + "friends_only": "\u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0641\u0642\u0637", + "groups_only": "\u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0627\u062a \u0641\u0642\u0637", + "both": "\u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0648\u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0627\u062a" }, - "sort_by": "فرز حسب", - "reverse_order": "عكس الترتيب", - "search_by_name": "بحث بالاسم", - "no_friends_found": "لم يتم العثور على أصدقاء", - "no_groups_found": "لم يتم العثور على مجموعات", - "no_friends_or_groups_found": "لم يتم العثور على أصدقاء أو مجموعات", - "relationship": "العلاقة: ", - "unknown_group": "مجموعة غير معروفة", - "type_group_chat": "النوع: دردشة جماعية", - "clean_conversations": "تنظيف {count} محادثات", - "remove_friends": "إزالة {count} أصدقاء", - "clean_conversations_and_remove_friends": "تنظيف {count} محادثات وإزالة {count} أصدقاء", - "clean_group_conversations": "تنظيف {count} محادثات جماعية", - "clean_all_conversations": "تنظيف {count} محادثات", - "failed_to_fetch_conversations": "فشل جلب المحادثات: {error}", - "failed_to_fetch_friend_conversations": "فشل جلب محادثات الأصدقاء: {error}", - "failed_to_process": "فشلت معالجة {id}", - "deleted_messages": "{count} رسالة محذوفة", + "sort_by": "\u0641\u0631\u0632 \u062d\u0633\u0628", + "reverse_order": "\u0639\u0643\u0633 \u0627\u0644\u062a\u0631\u062a\u064a\u0628", + "search_by_name": "\u0628\u062d\u062b \u0628\u0627\u0644\u0627\u0633\u0645", + "no_friends_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0623\u0635\u062f\u0642\u0627\u0621", + "no_groups_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0645\u062c\u0645\u0648\u0639\u0627\u062a", + "no_friends_or_groups_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0623\u0635\u062f\u0642\u0627\u0621 \u0623\u0648 \u0645\u062c\u0645\u0648\u0639\u0627\u062a", + "relationship": "\u0627\u0644\u0639\u0644\u0627\u0642\u0629: ", + "unknown_group": "\u0645\u062c\u0645\u0648\u0639\u0629 \u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641\u0629", + "type_group_chat": "\u0627\u0644\u0646\u0648\u0639: \u062f\u0631\u062f\u0634\u0629 \u062c\u0645\u0627\u0639\u064a\u0629", + "clean_conversations": "\u062a\u0646\u0638\u064a\u0641 {count} \u0645\u062d\u0627\u062f\u062b\u0627\u062a", + "remove_friends": "\u0625\u0632\u0627\u0644\u0629 {count} \u0623\u0635\u062f\u0642\u0627\u0621", + "clean_conversations_and_remove_friends": "\u062a\u0646\u0638\u064a\u0641 {count} \u0645\u062d\u0627\u062f\u062b\u0627\u062a \u0648\u0625\u0632\u0627\u0644\u0629 {count} \u0623\u0635\u062f\u0642\u0627\u0621", + "clean_group_conversations": "\u062a\u0646\u0638\u064a\u0641 {count} \u0645\u062d\u0627\u062f\u062b\u0627\u062a \u062c\u0645\u0627\u0639\u064a\u0629", + "clean_all_conversations": "\u062a\u0646\u0638\u064a\u0641 {count} \u0645\u062d\u0627\u062f\u062b\u0627\u062a", + "failed_to_fetch_conversations": "\u0641\u0634\u0644 \u062c\u0644\u0628 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0627\u062a: {error}", + "failed_to_fetch_friend_conversations": "\u0641\u0634\u0644 \u062c\u0644\u0628 \u0645\u062d\u0627\u062f\u062b\u0627\u062a \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621: {error}", + "failed_to_process": "\u0641\u0634\u0644\u062a \u0645\u0639\u0627\u0644\u062c\u0629 {id}", + "deleted_messages": "{count} \u0631\u0633\u0627\u0644\u0629 \u0645\u062d\u0630\u0648\u0641\u0629", "filters": { - "all": "الكل", - "my_friends": "أصدقائي", - "blocked": "المحظورين", - "removed_me": "الذين أزالوني", - "suggested": "المقترحة", - "deleted": "المحذوفة", - "business_accounts": "الحسابات التجارية", - "streaks": "الستريك (Streaks)", - "non_streaks": "بدون ستريك", - "followed": "المُتابَعون", - "following": "المُتابِعون", - "incoming": "طلبات الصداقة", - "incoming_follower": "طلبات المتابعة", - "location_on_map": "الموقع على الخريطة" + "all": "\u0627\u0644\u0643\u0644", + "my_friends": "\u0623\u0635\u062f\u0642\u0627\u0626\u064a", + "blocked": "\u0627\u0644\u0645\u062d\u0638\u0648\u0631\u064a\u0646", + "removed_me": "\u0627\u0644\u0630\u064a\u0646 \u0623\u0632\u0627\u0644\u0648\u0646\u064a", + "suggested": "\u0627\u0644\u0645\u0642\u062a\u0631\u062d\u0629", + "deleted": "\u0627\u0644\u0645\u062d\u0630\u0648\u0641\u0629", + "business_accounts": "\u0627\u0644\u062d\u0633\u0627\u0628\u0627\u062a \u0627\u0644\u062a\u062c\u0627\u0631\u064a\u0629", + "streaks": "\u0627\u0644\u0633\u062a\u0631\u064a\u0643 (Streaks)", + "non_streaks": "\u0628\u062f\u0648\u0646 \u0633\u062a\u0631\u064a\u0643", + "followed": "\u0627\u0644\u0645\u064f\u062a\u0627\u0628\u064e\u0639\u0648\u0646", + "following": "\u0627\u0644\u0645\u064f\u062a\u0627\u0628\u0650\u0639\u0648\u0646", + "incoming": "\u0637\u0644\u0628\u0627\u062a \u0627\u0644\u0635\u062f\u0627\u0642\u0629", + "incoming_follower": "\u0637\u0644\u0628\u0627\u062a \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629", + "location_on_map": "\u0627\u0644\u0645\u0648\u0642\u0639 \u0639\u0644\u0649 \u0627\u0644\u062e\u0631\u064a\u0637\u0629" }, "sort_options": { - "none": "لا شيء", - "username": "اسم المستخدم", - "added_timestamp": "طابع وقت الإضافة", - "snap_score": "نقاط Snap", - "streak_length": "طول الستريك", - "most_messages_sent": "الأكثر إرسالاً للرسائل", - "most_recent_message": "أحدث رسالة", - "nearest_location": "أقرب موقع" + "none": "\u0644\u0627 \u0634\u064a\u0621", + "username": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "added_timestamp": "\u0637\u0627\u0628\u0639 \u0648\u0642\u062a \u0627\u0644\u0625\u0636\u0627\u0641\u0629", + "snap_score": "\u0646\u0642\u0627\u0637 Snap", + "streak_length": "\u0637\u0648\u0644 \u0627\u0644\u0633\u062a\u0631\u064a\u0643", + "most_messages_sent": "\u0627\u0644\u0623\u0643\u062b\u0631 \u0625\u0631\u0633\u0627\u0644\u0627\u064b \u0644\u0644\u0631\u0633\u0627\u0626\u0644", + "most_recent_message": "\u0623\u062d\u062f\u062b \u0631\u0633\u0627\u0644\u0629", + "nearest_location": "\u0623\u0642\u0631\u0628 \u0645\u0648\u0642\u0639" } }, "chat_export": { "exporter_dialog": { - "select_conversations_title": "تحديد المحادثات", - "text_field_selection": "{amount} محدد", - "text_field_selection_all": "الكل", - "export_file_format_title": "تنسيق ملف التصدير", - "sort_order_title": "ترتيب الرسائل", - "sort_order_newest_to_oldest": "الأحدث إلى الأقدم", - "sort_order_oldest_to_newest": "الأقدم إلى الأحدث", - "message_type_filter_title": "تصفية الرسائل حسب النوع", - "amount_of_messages_title": "عدد الرسائل (اتركه فارغاً للكل)", - "download_medias_title": "تنزيل الوسائط" + "select_conversations_title": "\u062a\u062d\u062f\u064a\u062f \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0627\u062a", + "text_field_selection": "{amount} \u0645\u062d\u062f\u062f", + "text_field_selection_all": "\u0627\u0644\u0643\u0644", + "export_file_format_title": "\u062a\u0646\u0633\u064a\u0642 \u0645\u0644\u0641 \u0627\u0644\u062a\u0635\u062f\u064a\u0631", + "sort_order_title": "\u062a\u0631\u062a\u064a\u0628 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "sort_order_newest_to_oldest": "\u0627\u0644\u0623\u062d\u062f\u062b \u0625\u0644\u0649 \u0627\u0644\u0623\u0642\u062f\u0645", + "sort_order_oldest_to_newest": "\u0627\u0644\u0623\u0642\u062f\u0645 \u0625\u0644\u0649 \u0627\u0644\u0623\u062d\u062f\u062b", + "message_type_filter_title": "\u062a\u0635\u0641\u064a\u0629 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u062d\u0633\u0628 \u0627\u0644\u0646\u0648\u0639", + "amount_of_messages_title": "\u0639\u062f\u062f \u0627\u0644\u0631\u0633\u0627\u0626\u0644 (\u0627\u062a\u0631\u0643\u0647 \u0641\u0627\u0631\u063a\u0627\u064b \u0644\u0644\u0643\u0644)", + "download_medias_title": "\u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637" }, - "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}..." + "dialog_negative_button": "\u0625\u0644\u063a\u0627\u0621", + "dialog_positive_button": "\u062a\u0635\u062f\u064a\u0631", + "exported_to": "\u062a\u0645 \u0627\u0644\u062a\u0635\u062f\u064a\u0631 \u0625\u0644\u0649 {path}", + "exporting_chats": "\u062c\u0627\u0631\u064a \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u062f\u0631\u062f\u0634\u0627\u062a...", + "processing_chats": "\u062c\u0627\u0631\u064a \u0645\u0639\u0627\u0644\u062c\u0629 {amount} \u0645\u062d\u0627\u062f\u062b\u0629...", + "export_fail": "\u0641\u0634\u0644 \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629 {conversation}", + "writing_output": "\u062c\u0627\u0631\u064a \u0643\u062a\u0627\u0628\u0629 \u0627\u0644\u0645\u062e\u0631\u062c\u0627\u062a...", + "finished": "\u062a\u0645! \u064a\u0645\u0643\u0646\u0643 \u0627\u0644\u0622\u0646 \u0625\u063a\u0644\u0627\u0642 \u0647\u0630\u0627 \u0627\u0644\u062d\u0648\u0627\u0631.", + "no_messages_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0631\u0633\u0627\u0626\u0644!", + "exporting_message": "\u062c\u0627\u0631\u064a \u062a\u0635\u062f\u064a\u0631 {conversation}..." }, "button": { - "ok": "موافق", - "positive": "نعم", - "negative": "لا", - "cancel": "إلغاء", - "save": "حفظ", - "open": "فتح", - "download": "تنزيل", - "import": "استيراد", - "send": "إرسال", - "restore_original": "استعادة الأصلي", - "convert_external_media": "تحويل الوسائط الخارجية" + "ok": "\u0645\u0648\u0627\u0641\u0642", + "positive": "\u0646\u0639\u0645", + "negative": "\u0644\u0627", + "cancel": "\u0625\u0644\u063a\u0627\u0621", + "save": "\u062d\u0641\u0638", + "open": "\u0641\u062a\u062d", + "download": "\u062a\u0646\u0632\u064a\u0644", + "import": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f", + "send": "\u0625\u0631\u0633\u0627\u0644", + "restore_original": "\u0627\u0633\u062a\u0639\u0627\u062f\u0629 \u0627\u0644\u0623\u0635\u0644\u064a", + "convert_external_media": "\u062a\u062d\u0648\u064a\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0627\u0644\u062e\u0627\u0631\u062c\u064a\u0629" }, "tracker_events": { - "conversation_enter": "دخول المحادثة", - "conversation_exit": "خروج من المحادثة", - "started_typing": "بدأ الكتابة", - "stopped_typing": "توقف عن الكتابة", - "started_speaking": "بدأ التحدث", - "stopped_speaking": "توقف عن التحدث", - "started_peeking": "بدأ التلصص (Peeking)", - "stopped_peeking": "توقف عن التلصص", - "message_read": "قراءة الرسالة", - "message_deleted": "حذف الرسالة", - "message_saved": "حفظ الرسالة", - "message_unsaved": "إلغاء حفظ الرسالة", - "message_edited": "تحرير الرسالة", - "message_reaction_add": "إضافة تفاعل الرسالة", - "message_reaction_remove": "إزالة تفاعل الرسالة", - "snap_opened": "فتح Snap", - "snap_replayed": "إعادة تشغيل Snap", - "snap_replayed_twice": "إعادة تشغيل Snap مرتين", - "snap_screenshot": "لقطة شاشة لـ Snap", - "snap_screen_record": "تسجيل شاشة لـ Snap", - "i_can_see_you": "أستطيع رؤيتك" + "conversation_enter": "\u062f\u062e\u0648\u0644 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "conversation_exit": "\u062e\u0631\u0648\u062c \u0645\u0646 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "started_typing": "\u0628\u062f\u0623 \u0627\u0644\u0643\u062a\u0627\u0628\u0629", + "stopped_typing": "\u062a\u0648\u0642\u0641 \u0639\u0646 \u0627\u0644\u0643\u062a\u0627\u0628\u0629", + "started_speaking": "\u0628\u062f\u0623 \u0627\u0644\u062a\u062d\u062f\u062b", + "stopped_speaking": "\u062a\u0648\u0642\u0641 \u0639\u0646 \u0627\u0644\u062a\u062d\u062f\u062b", + "started_peeking": "\u0628\u062f\u0623 \u0627\u0644\u062a\u0644\u0635\u0635 (Peeking)", + "stopped_peeking": "\u062a\u0648\u0642\u0641 \u0639\u0646 \u0627\u0644\u062a\u0644\u0635\u0635", + "message_read": "\u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "message_deleted": "\u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "message_saved": "\u062d\u0641\u0638 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "message_unsaved": "\u0625\u0644\u063a\u0627\u0621 \u062d\u0641\u0638 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "message_edited": "\u062a\u062d\u0631\u064a\u0631 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "message_reaction_add": "\u0625\u0636\u0627\u0641\u0629 \u062a\u0641\u0627\u0639\u0644 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "message_reaction_remove": "\u0625\u0632\u0627\u0644\u0629 \u062a\u0641\u0627\u0639\u0644 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "snap_opened": "\u0641\u062a\u062d Snap", + "snap_replayed": "\u0625\u0639\u0627\u062f\u0629 \u062a\u0634\u063a\u064a\u0644 Snap", + "snap_replayed_twice": "\u0625\u0639\u0627\u062f\u0629 \u062a\u0634\u063a\u064a\u0644 Snap \u0645\u0631\u062a\u064a\u0646", + "snap_screenshot": "\u0644\u0642\u0637\u0629 \u0634\u0627\u0634\u0629 \u0644\u0640 Snap", + "snap_screen_record": "\u062a\u0633\u062c\u064a\u0644 \u0634\u0627\u0634\u0629 \u0644\u0640 Snap", + "i_can_see_you": "\u0623\u0633\u062a\u0637\u064a\u0639 \u0631\u0624\u064a\u062a\u0643" }, - "cleared_from_feed": "تم المسح من الموجز", + "cleared_from_feed": "\u062a\u0645 \u0627\u0644\u0645\u0633\u062d \u0645\u0646 \u0627\u0644\u0645\u0648\u062c\u0632", "tracker_actions": { - "log": "سجل", - "in_app_notification": "إشعار داخل التطبيق", - "push_notification": "إشعار دفع (Push)", - "custom": "مخصص" + "log": "\u0633\u062c\u0644", + "in_app_notification": "\u0625\u0634\u0639\u0627\u0631 \u062f\u0627\u062e\u0644 \u0627\u0644\u062a\u0637\u0628\u064a\u0642", + "push_notification": "\u0625\u0634\u0639\u0627\u0631 \u062f\u0641\u0639 (Push)", + "custom": "\u0645\u062e\u0635\u0635" }, "better_notifications": { "button": { - "reply": "رد", - "download": "تنزيل", - "mark_as_read": "وضع علامة كمقروء" + "reply": "\u0631\u062f", + "download": "\u062a\u0646\u0632\u064a\u0644", + "mark_as_read": "\u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0643\u0645\u0642\u0631\u0648\u0621" } }, "profile_picture_downloader": { - "button": "تنزيل صورة الملف الشخصي", - "title": "أداة تنزيل صورة الملف الشخصي", - "avatar_option": "أفاتار", - "background_option": "خلفية" + "button": "\u062a\u0646\u0632\u064a\u0644 \u0635\u0648\u0631\u0629 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a", + "title": "\u0623\u062f\u0627\u0629 \u062a\u0646\u0632\u064a\u0644 \u0635\u0648\u0631\u0629 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a", + "avatar_option": "\u0623\u0641\u0627\u062a\u0627\u0631", + "background_option": "\u062e\u0644\u0641\u064a\u0629" }, "call_start_confirmation": { - "dialog_title": "بدء مكالمة", - "dialog_message": "هل أنت متأكد أنك تريد بدء مكالمة؟" + "dialog_title": "\u0628\u062f\u0621 \u0645\u0643\u0627\u0644\u0645\u0629", + "dialog_message": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u0628\u062f\u0621 \u0645\u0643\u0627\u0644\u0645\u0629\u061f" }, "half_swipe_notifier": { - "notification_channel_name": "التمرير النصفي", - "notification_content_dm": "{friend} قام بالتمرير النصفي في دردشتك لمدة {duration} ثانية", - "notification_content_group": "{friend} قام بالتمرير النصفي في {group} لمدة {duration} ثانية" + "notification_channel_name": "\u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a", + "notification_content_dm": "{friend} \u0642\u0627\u0645 \u0628\u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a \u0641\u064a \u062f\u0631\u062f\u0634\u062a\u0643 \u0644\u0645\u062f\u0629 {duration} \u062b\u0627\u0646\u064a\u0629", + "notification_content_group": "{friend} \u0642\u0627\u0645 \u0628\u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0627\u0644\u0646\u0635\u0641\u064a \u0641\u064a {group} \u0644\u0645\u062f\u0629 {duration} \u062b\u0627\u0646\u064a\u0629" }, "download_processor": { "attachment_type": { "snap": "Snap", - "sticker": "ملصق", + "sticker": "\u0645\u0644\u0635\u0642", "gif": "GIF", - "external_media": "وسائط خارجية", - "note": "ملاحظة", - "original_story": "قصة أصلية" + "external_media": "\u0648\u0633\u0627\u0626\u0637 \u062e\u0627\u0631\u062c\u064a\u0629", + "note": "\u0645\u0644\u0627\u062d\u0638\u0629", + "original_story": "\u0642\u0635\u0629 \u0623\u0635\u0644\u064a\u0629" }, - "select_attachments_title": "تحديد المرفقات", - "download_started_toast": "بدأ التنزيل", - "unsupported_content_type_toast": "نوع المحتوى غير مدعوم!", - "failed_no_longer_available_toast": "الوسائط لم تعد متوفرة", - "no_attachments_toast": "لم يتم العثور على مرفقات!", - "already_queued_toast": "الوسائط في قائمة الانتظار بالفعل!", - "already_downloaded_toast": "تم تنزيل الوسائط بالفعل!", - "content_saved_toast": "تم الحفظ!", - "download_toast": "جاري تنزيل {path}...", - "processing_toast": "جاري معالجة {path}...", - "failed_generic_toast": "فشل التنزيل", - "failed_to_create_preview_toast": "فشل إنشاء المعاينة", - "failed_processing_toast": "فشلت المعالجة {error}", - "failed_gallery_toast": "فشل الحفظ في المعرض {error}", - "dash_no_chapter": "لم يتم العثور على فصل", + "select_attachments_title": "\u062a\u062d\u062f\u064a\u062f \u0627\u0644\u0645\u0631\u0641\u0642\u0627\u062a", + "download_started_toast": "\u0628\u062f\u0623 \u0627\u0644\u062a\u0646\u0632\u064a\u0644", + "unsupported_content_type_toast": "\u0646\u0648\u0639 \u0627\u0644\u0645\u062d\u062a\u0648\u0649 \u063a\u064a\u0631 \u0645\u062f\u0639\u0648\u0645!", + "failed_no_longer_available_toast": "\u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0644\u0645 \u062a\u0639\u062f \u0645\u062a\u0648\u0641\u0631\u0629", + "no_attachments_toast": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0645\u0631\u0641\u0642\u0627\u062a!", + "already_queued_toast": "\u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0641\u064a \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631 \u0628\u0627\u0644\u0641\u0639\u0644!", + "already_downloaded_toast": "\u062a\u0645 \u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0628\u0627\u0644\u0641\u0639\u0644!", + "content_saved_toast": "\u062a\u0645 \u0627\u0644\u062d\u0641\u0638!", + "download_toast": "\u062c\u0627\u0631\u064a \u062a\u0646\u0632\u064a\u0644 {path}...", + "processing_toast": "\u062c\u0627\u0631\u064a \u0645\u0639\u0627\u0644\u062c\u0629 {path}...", + "failed_generic_toast": "\u0641\u0634\u0644 \u0627\u0644\u062a\u0646\u0632\u064a\u0644", + "failed_to_create_preview_toast": "\u0641\u0634\u0644 \u0625\u0646\u0634\u0627\u0621 \u0627\u0644\u0645\u0639\u0627\u064a\u0646\u0629", + "failed_processing_toast": "\u0641\u0634\u0644\u062a \u0627\u0644\u0645\u0639\u0627\u0644\u062c\u0629 {error}", + "failed_gallery_toast": "\u0641\u0634\u0644 \u0627\u0644\u062d\u0641\u0638 \u0641\u064a \u0627\u0644\u0645\u0639\u0631\u0636 {error}", + "dash_no_chapter": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0641\u0635\u0644", "dash_dialog": { - "title": "تنزيل وسائط dash", - "download_all": "تنزيل الكل", - "segment_text": "جزء {from} - {to}" + "title": "\u062a\u0646\u0632\u064a\u0644 \u0648\u0633\u0627\u0626\u0637 dash", + "download_all": "\u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0643\u0644", + "segment_text": "\u062c\u0632\u0621 {from} - {to}" }, "story_snap_dialog": { - "title": "تنزيل سنابات القصة", - "select_all": "تحديد الكل", - "deselect_all": "إلغاء التحديد", - "snap_item": "السناب {index} من {total}" + "title": "\u062a\u0646\u0632\u064a\u0644 \u0633\u0646\u0627\u0628\u0627\u062a \u0627\u0644\u0642\u0635\u0629", + "select_all": "\u062a\u062d\u062f\u064a\u062f \u0627\u0644\u0643\u0644", + "deselect_all": "\u0625\u0644\u063a\u0627\u0621 \u0627\u0644\u062a\u062d\u062f\u064a\u062f", + "snap_item": "\u0627\u0644\u0633\u0646\u0627\u0628 {index} \u0645\u0646 {total}" }, - "batch_download_complete_toast": "تم تنزيل جميع السنابات", - "batch_download_jump_failed_toast": "تعذر الانتقال للسناب التالي. تأكد من أن عرض القصة مرئي." + "batch_download_complete_toast": "\u062a\u0645 \u062a\u0646\u0632\u064a\u0644 \u062c\u0645\u064a\u0639 \u0627\u0644\u0633\u0646\u0627\u0628\u0627\u062a", + "batch_download_jump_failed_toast": "\u062a\u0639\u0630\u0631 \u0627\u0644\u0627\u0646\u062a\u0642\u0627\u0644 \u0644\u0644\u0633\u0646\u0627\u0628 \u0627\u0644\u062a\u0627\u0644\u064a. \u062a\u0623\u0643\u062f \u0645\u0646 \u0623\u0646 \u0639\u0631\u0636 \u0627\u0644\u0642\u0635\u0629 \u0645\u0631\u0626\u064a." }, "streaks_reminder": { - "notification_title": "الستريك (Streaks)", - "notification_text": "ستفقد الستريك مع {friend} خلال {hoursLeft} ساعة" + "notification_title": "\u0627\u0644\u0633\u062a\u0631\u064a\u0643 (Streaks)", + "notification_text": "\u0633\u062a\u0641\u0642\u062f \u0627\u0644\u0633\u062a\u0631\u064a\u0643 \u0645\u0639 {friend} \u062e\u0644\u0627\u0644 {hoursLeft} \u0633\u0627\u0639\u0629" }, "biometric_auth": { - "unlock_button": "فتح القفل", - "title": "فتح قفل Snapchat", - "subtitle": "يرجى المصادقة لفتح قفل Snapchat" + "unlock_button": "\u0641\u062a\u062d \u0627\u0644\u0642\u0641\u0644", + "title": "\u0641\u062a\u062d \u0642\u0641\u0644 Snapchat", + "subtitle": "\u064a\u0631\u062c\u0649 \u0627\u0644\u0645\u0635\u0627\u062f\u0642\u0629 \u0644\u0641\u062a\u062d \u0642\u0641\u0644 Snapchat" }, "end_to_end_encryption": { "toolbox": { - "no_shared_key": "ليس لديك سر مشترك مع هذا الصديق بعد. انقر أدناه لبدء واحد جديد.", - "shared_key_fingerprint": "بصمتك هي:\n\n{fingerprint}\n\nتأكد من التحقق مما إذا كانت تطابق بصمة صديقك!", - "initiate_exchange_button": "بدء تبادل المفاتيح" + "no_shared_key": "\u0644\u064a\u0633 \u0644\u062f\u064a\u0643 \u0633\u0631 \u0645\u0634\u062a\u0631\u0643 \u0645\u0639 \u0647\u0630\u0627 \u0627\u0644\u0635\u062f\u064a\u0642 \u0628\u0639\u062f. \u0627\u0646\u0642\u0631 \u0623\u062f\u0646\u0627\u0647 \u0644\u0628\u062f\u0621 \u0648\u0627\u062d\u062f \u062c\u062f\u064a\u062f.", + "shared_key_fingerprint": "\u0628\u0635\u0645\u062a\u0643 \u0647\u064a:\n\n{fingerprint}\n\n\u062a\u0623\u0643\u062f \u0645\u0646 \u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0645\u0627 \u0625\u0630\u0627 \u0643\u0627\u0646\u062a \u062a\u0637\u0627\u0628\u0642 \u0628\u0635\u0645\u0629 \u0635\u062f\u064a\u0642\u0643!", + "initiate_exchange_button": "\u0628\u062f\u0621 \u062a\u0628\u0627\u062f\u0644 \u0627\u0644\u0645\u0641\u0627\u062a\u064a\u062d" }, "confirmation_dialogs": { - "title": "التشفير من طرف لطرف", - "confirmation_1": "تحذير: سيؤدي هذا إلى استبدال مفتاحك الحالي. ستفقد الوصول إلى جميع الرسائل المشفرة من هذا الصديق. هل أنت متأكد أنك تريد المتابعة؟", - "confirmation_2": "هل أنت متأكد حقاً أنك تريد المتابعة؟ هذه فرصتك الأخيرة للتراجع." + "title": "\u0627\u0644\u062a\u0634\u0641\u064a\u0631 \u0645\u0646 \u0637\u0631\u0641 \u0644\u0637\u0631\u0641", + "confirmation_1": "\u062a\u062d\u0630\u064a\u0631: \u0633\u064a\u0624\u062f\u064a \u0647\u0630\u0627 \u0625\u0644\u0649 \u0627\u0633\u062a\u0628\u062f\u0627\u0644 \u0645\u0641\u062a\u0627\u062d\u0643 \u0627\u0644\u062d\u0627\u0644\u064a. \u0633\u062a\u0641\u0642\u062f \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u062c\u0645\u064a\u0639 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0634\u0641\u0631\u0629 \u0645\u0646 \u0647\u0630\u0627 \u0627\u0644\u0635\u062f\u064a\u0642. \u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629\u061f", + "confirmation_2": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u062d\u0642\u0627\u064b \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629\u061f \u0647\u0630\u0647 \u0641\u0631\u0635\u062a\u0643 \u0627\u0644\u0623\u062e\u064a\u0631\u0629 \u0644\u0644\u062a\u0631\u0627\u062c\u0639." }, - "unencrypted_conversation_send_failure_toast": "لا يمكنك إرسال محتوى مشفر لكل من المحادثات المشفرة وغير المشفرة!", - "native_hooks_send_failure_toast": "فشل الإرسال! يرجى تمكين Native Hooks في الإعدادات.", - "no_participants_to_encrypt_toast": "ليس لديك أي أصدقاء في هذه المحادثة لتشفير الرسائل معهم!", - "encryption_failed_toast": "فشل تشفير الرسالة! تحقق من logcat لمزيد من التفاصيل.", - "missing_friend_id_toast": "لا يمكن العثور على friendId لـ conversationId {conversationId}", - "key_exchange_failed_toast": "لا يمكن إنشاء تبادل مفاتيح لـ friendId {friendId}", - "accept_public_key_success_toast": "تم قبول المفتاح العام بنجاح!", - "accept_secret_key_success_toast": "تم! يمكنك الآن إرسال واستقبال الرسائل المشفرة مع هذا الصديق.", - "accept_public_key_failure_toast": "فشل قبول المفتاح العام", - "accept_secret_key_failure_toast": "فشل قبول المفتاح السري", - "accept_secret_button": "قبول السري", - "accept_public_key_button": "قبول المفتاح العام", - "outgoing_pk_message": "طلب تبادل المفاتيح", - "outgoing_secret_message": "استجابة تبادل المفاتيح", - "incoming_pk_message": "لقد تلقيت للتو طلب مفتاح عام. انقر أدناه لقبوله.", - "incoming_secret_message": "قبل صديقك للتو مفتاحك العام. انقر أدناه لقبول السر." + "unencrypted_conversation_send_failure_toast": "\u0644\u0627 \u064a\u0645\u0643\u0646\u0643 \u0625\u0631\u0633\u0627\u0644 \u0645\u062d\u062a\u0648\u0649 \u0645\u0634\u0641\u0631 \u0644\u0643\u0644 \u0645\u0646 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0627\u062a \u0627\u0644\u0645\u0634\u0641\u0631\u0629 \u0648\u063a\u064a\u0631 \u0627\u0644\u0645\u0634\u0641\u0631\u0629!", + "native_hooks_send_failure_toast": "\u0641\u0634\u0644 \u0627\u0644\u0625\u0631\u0633\u0627\u0644! \u064a\u0631\u062c\u0649 \u062a\u0645\u0643\u064a\u0646 Native Hooks \u0641\u064a \u0627\u0644\u0625\u0639\u062f\u0627\u062f\u0627\u062a.", + "no_participants_to_encrypt_toast": "\u0644\u064a\u0633 \u0644\u062f\u064a\u0643 \u0623\u064a \u0623\u0635\u062f\u0642\u0627\u0621 \u0641\u064a \u0647\u0630\u0647 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629 \u0644\u062a\u0634\u0641\u064a\u0631 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0645\u0639\u0647\u0645!", + "encryption_failed_toast": "\u0641\u0634\u0644 \u062a\u0634\u0641\u064a\u0631 \u0627\u0644\u0631\u0633\u0627\u0644\u0629! \u062a\u062d\u0642\u0642 \u0645\u0646 logcat \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644.", + "missing_friend_id_toast": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 friendId \u0644\u0640 conversationId {conversationId}", + "key_exchange_failed_toast": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u0625\u0646\u0634\u0627\u0621 \u062a\u0628\u0627\u062f\u0644 \u0645\u0641\u0627\u062a\u064a\u062d \u0644\u0640 friendId {friendId}", + "accept_public_key_success_toast": "\u062a\u0645 \u0642\u0628\u0648\u0644 \u0627\u0644\u0645\u0641\u062a\u0627\u062d \u0627\u0644\u0639\u0627\u0645 \u0628\u0646\u062c\u0627\u062d!", + "accept_secret_key_success_toast": "\u062a\u0645! \u064a\u0645\u0643\u0646\u0643 \u0627\u0644\u0622\u0646 \u0625\u0631\u0633\u0627\u0644 \u0648\u0627\u0633\u062a\u0642\u0628\u0627\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0627\u0644\u0645\u0634\u0641\u0631\u0629 \u0645\u0639 \u0647\u0630\u0627 \u0627\u0644\u0635\u062f\u064a\u0642.", + "accept_public_key_failure_toast": "\u0641\u0634\u0644 \u0642\u0628\u0648\u0644 \u0627\u0644\u0645\u0641\u062a\u0627\u062d \u0627\u0644\u0639\u0627\u0645", + "accept_secret_key_failure_toast": "\u0641\u0634\u0644 \u0642\u0628\u0648\u0644 \u0627\u0644\u0645\u0641\u062a\u0627\u062d \u0627\u0644\u0633\u0631\u064a", + "accept_secret_button": "\u0642\u0628\u0648\u0644 \u0627\u0644\u0633\u0631\u064a", + "accept_public_key_button": "\u0642\u0628\u0648\u0644 \u0627\u0644\u0645\u0641\u062a\u0627\u062d \u0627\u0644\u0639\u0627\u0645", + "outgoing_pk_message": "\u0637\u0644\u0628 \u062a\u0628\u0627\u062f\u0644 \u0627\u0644\u0645\u0641\u0627\u062a\u064a\u062d", + "outgoing_secret_message": "\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u062a\u0628\u0627\u062f\u0644 \u0627\u0644\u0645\u0641\u0627\u062a\u064a\u062d", + "incoming_pk_message": "\u0644\u0642\u062f \u062a\u0644\u0642\u064a\u062a \u0644\u0644\u062a\u0648 \u0637\u0644\u0628 \u0645\u0641\u062a\u0627\u062d \u0639\u0627\u0645. \u0627\u0646\u0642\u0631 \u0623\u062f\u0646\u0627\u0647 \u0644\u0642\u0628\u0648\u0644\u0647.", + "incoming_secret_message": "\u0642\u0628\u0644 \u0635\u062f\u064a\u0642\u0643 \u0644\u0644\u062a\u0648 \u0645\u0641\u062a\u0627\u062d\u0643 \u0627\u0644\u0639\u0627\u0645. \u0627\u0646\u0642\u0631 \u0623\u062f\u0646\u0627\u0647 \u0644\u0642\u0628\u0648\u0644 \u0627\u0644\u0633\u0631." }, "account_switcher_ui": { - "already_logged_in": "تم تسجيل الدخول بالفعل كـ {username}", - "login_failed_toast": "فشل تسجيل الدخول. تحقق من السجلات لمزيد من المعلومات.", - "logged_out_toast": "تم تسجيل الخروج", - "data_not_found_toast": "لم يتم العثور على بيانات الحساب", - "restore_failed_toast": "فشل استعادة بيانات الحساب", - "logged_in_as_toast": "تم تسجيل الدخول كـ {username}", - "backup_success_toast": "تم النسخ الاحتياطي للحساب!", - "backup_failure_toast": "فشل النسخ الاحتياطي للحساب. تحقق من السجلات لمزيد من المعلومات.", - "import_success_toast": "تم استيراد {username}!", - "import_failure_toast": "فشل استيراد الحساب: {message}", - "export_success_toast": "تم تصدير الحساب!", - "export_failed_toast": "فشل تصدير الحساب. تحقق من السجلات لمزيد من المعلومات.", - "forced_logout_toast": "تمت إزالة الحساب بسبب تسجيل الخروج الإجباري" + "already_logged_in": "\u062a\u0645 \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644 \u0628\u0627\u0644\u0641\u0639\u0644 \u0643\u0640 {username}", + "login_failed_toast": "\u0641\u0634\u0644 \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u0645\u0639\u0644\u0648\u0645\u0627\u062a.", + "logged_out_toast": "\u062a\u0645 \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062e\u0631\u0648\u062c", + "data_not_found_toast": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u062d\u0633\u0627\u0628", + "restore_failed_toast": "\u0641\u0634\u0644 \u0627\u0633\u062a\u0639\u0627\u062f\u0629 \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u062d\u0633\u0627\u0628", + "logged_in_as_toast": "\u062a\u0645 \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644 \u0643\u0640 {username}", + "backup_success_toast": "\u062a\u0645 \u0627\u0644\u0646\u0633\u062e \u0627\u0644\u0627\u062d\u062a\u064a\u0627\u0637\u064a \u0644\u0644\u062d\u0633\u0627\u0628!", + "backup_failure_toast": "\u0641\u0634\u0644 \u0627\u0644\u0646\u0633\u062e \u0627\u0644\u0627\u062d\u062a\u064a\u0627\u0637\u064a \u0644\u0644\u062d\u0633\u0627\u0628. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u0645\u0639\u0644\u0648\u0645\u0627\u062a.", + "import_success_toast": "\u062a\u0645 \u0627\u0633\u062a\u064a\u0631\u0627\u062f {username}!", + "import_failure_toast": "\u0641\u0634\u0644 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u062d\u0633\u0627\u0628: {message}", + "export_success_toast": "\u062a\u0645 \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u062d\u0633\u0627\u0628!", + "export_failed_toast": "\u0641\u0634\u0644 \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u062d\u0633\u0627\u0628. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u0645\u0639\u0644\u0648\u0645\u0627\u062a.", + "forced_logout_toast": "\u062a\u0645\u062a \u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u062d\u0633\u0627\u0628 \u0628\u0633\u0628\u0628 \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062e\u0631\u0648\u062c \u0627\u0644\u0625\u062c\u0628\u0627\u0631\u064a" }, "auto_open_snaps": { - "title": "فتح الـ Snaps تلقائياً", - "priority_title": "فتح الـ Snaps تلقائياً (أولوية)", - "error_title": "فتح الـ Snaps تلقائياً (أخطاء)", - "channel_description": "إشعارات لحالة قائمة انتظار فتح الـ snaps تلقائياً", - "priority_channel_description": "إشعارات عالية الأولوية لفتح الـ snaps تلقائياً", - "error_channel_description": "إشعارات الخطأ عند فشل فتح الـ snaps تلقائياً", - "paused_status": "فتح الـ Snaps تلقائياً متوقف مؤقتاً", - "processing_status": "معالجة الـ snaps: {queued} في قائمة الانتظار، {processed} تمت معالجتها", - "monitor_status": "جاري المراقبة...", - "recent_snaps": "Snaps الحديثة", - "action_pause": "إيقاف مؤقت", - "action_resume": "استئناف", - "action_clear": "مسح قائمة الانتظار", - "action_reset": "إعادة تعيين العد", - "error_content": "فشل فتح snap من {sender}: {error}", - "resumed_feedback": "تم استئناف الفتح التلقائي", - "paused_feedback": "تم إيقاف الفتح التلقائي مؤقتاً", - "resumed_message": "ستستمر المعالجة تلقائياً للـ snaps في قائمة الانتظار", - "paused_message": "المعالجة متوقفة مؤقتاً. تم حفظ قائمة الانتظار ({count} snaps)", - "status_paused": "متوقف مؤقتاً", - "status_monitoring": "جاري المراقبة", - "status_active": "نشط", - "queue_cleared": "تم مسح قائمة الانتظار وإعادة تعيين الإحصائيات", - "queue_cleared_title": "تم مسح قائمة الانتظار", - "queue_cleared_reset": "مسح قائمة الانتظار وإعادة التعيين", - "queue_cleared_feedback": "تم مسح {count} snaps في الانتظار \u2022 تمت إعادة تعيين {processed} عد المعالجة", - "queue_cleared_feedback_simple": "تمت إعادة تعيين {processed} عد المعالجة", - "unknown_sender": "غير معروف", - "unknown_user": "مستخدم غير معروف", - "content_type_external_media": "وسائط خارجية", + "title": "\u0641\u062a\u062d \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "priority_title": "\u0641\u062a\u062d \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b (\u0623\u0648\u0644\u0648\u064a\u0629)", + "error_title": "\u0641\u062a\u062d \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b (\u0623\u062e\u0637\u0627\u0621)", + "channel_description": "\u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0644\u062d\u0627\u0644\u0629 \u0642\u0627\u0626\u0645\u0629 \u0627\u0646\u062a\u0638\u0627\u0631 \u0641\u062a\u062d \u0627\u0644\u0640 snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "priority_channel_description": "\u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0639\u0627\u0644\u064a\u0629 \u0627\u0644\u0623\u0648\u0644\u0648\u064a\u0629 \u0644\u0641\u062a\u062d \u0627\u0644\u0640 snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "error_channel_description": "\u0625\u0634\u0639\u0627\u0631\u0627\u062a \u0627\u0644\u062e\u0637\u0623 \u0639\u0646\u062f \u0641\u0634\u0644 \u0641\u062a\u062d \u0627\u0644\u0640 snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b", + "paused_status": "\u0641\u062a\u062d \u0627\u0644\u0640 Snaps \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0645\u062a\u0648\u0642\u0641 \u0645\u0624\u0642\u062a\u0627\u064b", + "processing_status": "\u0645\u0639\u0627\u0644\u062c\u0629 \u0627\u0644\u0640 snaps: {queued} \u0641\u064a \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631\u060c {processed} \u062a\u0645\u062a \u0645\u0639\u0627\u0644\u062c\u062a\u0647\u0627", + "monitor_status": "\u062c\u0627\u0631\u064a \u0627\u0644\u0645\u0631\u0627\u0642\u0628\u0629...", + "recent_snaps": "Snaps \u0627\u0644\u062d\u062f\u064a\u062b\u0629", + "action_pause": "\u0625\u064a\u0642\u0627\u0641 \u0645\u0624\u0642\u062a", + "action_resume": "\u0627\u0633\u062a\u0626\u0646\u0627\u0641", + "action_clear": "\u0645\u0633\u062d \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631", + "action_reset": "\u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646 \u0627\u0644\u0639\u062f", + "error_content": "\u0641\u0634\u0644 \u0641\u062a\u062d snap \u0645\u0646 {sender}: {error}", + "resumed_feedback": "\u062a\u0645 \u0627\u0633\u062a\u0626\u0646\u0627\u0641 \u0627\u0644\u0641\u062a\u062d \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "paused_feedback": "\u062a\u0645 \u0625\u064a\u0642\u0627\u0641 \u0627\u0644\u0641\u062a\u062d \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0645\u0624\u0642\u062a\u0627\u064b", + "resumed_message": "\u0633\u062a\u0633\u062a\u0645\u0631 \u0627\u0644\u0645\u0639\u0627\u0644\u062c\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b \u0644\u0644\u0640 snaps \u0641\u064a \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631", + "paused_message": "\u0627\u0644\u0645\u0639\u0627\u0644\u062c\u0629 \u0645\u062a\u0648\u0642\u0641\u0629 \u0645\u0624\u0642\u062a\u0627\u064b. \u062a\u0645 \u062d\u0641\u0638 \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631 ({count} snaps)", + "status_paused": "\u0645\u062a\u0648\u0642\u0641 \u0645\u0624\u0642\u062a\u0627\u064b", + "status_monitoring": "\u062c\u0627\u0631\u064a \u0627\u0644\u0645\u0631\u0627\u0642\u0628\u0629", + "status_active": "\u0646\u0634\u0637", + "queue_cleared": "\u062a\u0645 \u0645\u0633\u062d \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631 \u0648\u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646 \u0627\u0644\u0625\u062d\u0635\u0627\u0626\u064a\u0627\u062a", + "queue_cleared_title": "\u062a\u0645 \u0645\u0633\u062d \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631", + "queue_cleared_reset": "\u0645\u0633\u062d \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631 \u0648\u0625\u0639\u0627\u062f\u0629 \u0627\u0644\u062a\u0639\u064a\u064a\u0646", + "queue_cleared_feedback": "\u062a\u0645 \u0645\u0633\u062d {count} snaps \u0641\u064a \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631 \u2022 \u062a\u0645\u062a \u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646 {processed} \u0639\u062f \u0627\u0644\u0645\u0639\u0627\u0644\u062c\u0629", + "queue_cleared_feedback_simple": "\u062a\u0645\u062a \u0625\u0639\u0627\u062f\u0629 \u062a\u0639\u064a\u064a\u0646 {processed} \u0639\u062f \u0627\u0644\u0645\u0639\u0627\u0644\u062c\u0629", + "unknown_sender": "\u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641", + "unknown_user": "\u0645\u0633\u062a\u062e\u062f\u0645 \u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641", + "content_type_external_media": "\u0648\u0633\u0627\u0626\u0637 \u062e\u0627\u0631\u062c\u064a\u0629", "content_type_snap": "Snap", - "conversation_type_friend_dm": "DM صديق", + "conversation_type_friend_dm": "DM \u0635\u062f\u064a\u0642", "conversation_type_dm": "DM", - "conversation_type_group_chat": "دردشة جماعية", - "conversation_type_chat": "دردشة", - "notification_status": "الحالة", - "notification_statistics": "الإحصائيات", - "notification_queue_size": "حجم قائمة الانتظار", - "notification_total_opened": "إجمالي الـ Snaps المفتوحة", - "notification_queue_preview": "معاينة قائمة الانتظار", - "notification_processing_continue": "ستستمر المعالجة تلقائياً...", - "notification_no_snaps_queue": "لا توجد snaps في قائمة الانتظار.", - "notification_queue_cleared_opened": "تم مسح قائمة الانتظار ({opened} مفتوح)", - "content_type_photo_video_snap": "Snap صورة/فيديو", - "conversation_type_group_with_name": "مجموعة: {name}", - "delete_logs_title": "حذف السجلات؟", - "delete_logs_progress": "جاري حذف {count} سجل...", - "delete_logs_description": "سيؤدي هذا إلى حذف السجلات بناءً على الفلتر الحالي واستعلام البحث. هذا الإجراء لا يمكن التراجع عنه.", - "export_logs_title": "تصدير السجلات؟", - "export_logs_progress": "جاري تصدير السجلات...", - "export_logs_description": "سيؤدي هذا إلى تصدير السجلات بناءً على الفلتر الحالي واستعلام البحث.", - "export_logs_as": "تصدير كـ {type}", - "export_logs_success": "تم تصدير السجلات!", - "export_logs_failure": "فشل تصدير السجلات. تحقق من logcat لمزيد من التفاصيل.", - "deleted_logs_count": "تم حذف {count} سجل" + "conversation_type_group_chat": "\u062f\u0631\u062f\u0634\u0629 \u062c\u0645\u0627\u0639\u064a\u0629", + "conversation_type_chat": "\u062f\u0631\u062f\u0634\u0629", + "notification_status": "\u0627\u0644\u062d\u0627\u0644\u0629", + "notification_statistics": "\u0627\u0644\u0625\u062d\u0635\u0627\u0626\u064a\u0627\u062a", + "notification_queue_size": "\u062d\u062c\u0645 \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631", + "notification_total_opened": "\u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0640 Snaps \u0627\u0644\u0645\u0641\u062a\u0648\u062d\u0629", + "notification_queue_preview": "\u0645\u0639\u0627\u064a\u0646\u0629 \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631", + "notification_processing_continue": "\u0633\u062a\u0633\u062a\u0645\u0631 \u0627\u0644\u0645\u0639\u0627\u0644\u062c\u0629 \u062a\u0644\u0642\u0627\u0626\u064a\u0627\u064b...", + "notification_no_snaps_queue": "\u0644\u0627 \u062a\u0648\u062c\u062f snaps \u0641\u064a \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631.", + "notification_queue_cleared_opened": "\u062a\u0645 \u0645\u0633\u062d \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631 ({opened} \u0645\u0641\u062a\u0648\u062d)", + "content_type_photo_video_snap": "Snap \u0635\u0648\u0631\u0629/\u0641\u064a\u062f\u064a\u0648", + "conversation_type_group_with_name": "\u0645\u062c\u0645\u0648\u0639\u0629: {name}", + "delete_logs_title": "\u062d\u0630\u0641 \u0627\u0644\u0633\u062c\u0644\u0627\u062a\u061f", + "delete_logs_progress": "\u062c\u0627\u0631\u064a \u062d\u0630\u0641 {count} \u0633\u062c\u0644...", + "delete_logs_description": "\u0633\u064a\u0624\u062f\u064a \u0647\u0630\u0627 \u0625\u0644\u0649 \u062d\u0630\u0641 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0628\u0646\u0627\u0621\u064b \u0639\u0644\u0649 \u0627\u0644\u0641\u0644\u062a\u0631 \u0627\u0644\u062d\u0627\u0644\u064a \u0648\u0627\u0633\u062a\u0639\u0644\u0627\u0645 \u0627\u0644\u0628\u062d\u062b. \u0647\u0630\u0627 \u0627\u0644\u0625\u062c\u0631\u0627\u0621 \u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u062a\u0631\u0627\u062c\u0639 \u0639\u0646\u0647.", + "export_logs_title": "\u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0633\u062c\u0644\u0627\u062a\u061f", + "export_logs_progress": "\u062c\u0627\u0631\u064a \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0633\u062c\u0644\u0627\u062a...", + "export_logs_description": "\u0633\u064a\u0624\u062f\u064a \u0647\u0630\u0627 \u0625\u0644\u0649 \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0628\u0646\u0627\u0621\u064b \u0639\u0644\u0649 \u0627\u0644\u0641\u0644\u062a\u0631 \u0627\u0644\u062d\u0627\u0644\u064a \u0648\u0627\u0633\u062a\u0639\u0644\u0627\u0645 \u0627\u0644\u0628\u062d\u062b.", + "export_logs_as": "\u062a\u0635\u062f\u064a\u0631 \u0643\u0640 {type}", + "export_logs_success": "\u062a\u0645 \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0633\u062c\u0644\u0627\u062a!", + "export_logs_failure": "\u0641\u0634\u0644 \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0633\u062c\u0644\u0627\u062a. \u062a\u062d\u0642\u0642 \u0645\u0646 logcat \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644.", + "deleted_logs_count": "\u062a\u0645 \u062d\u0630\u0641 {count} \u0633\u062c\u0644" }, - "script_imported": "تم استيراد السكربت {name}!", - "script_import_failed": "فشل استيراد السكربت. {error}. تحقق من السجلات لمزيد من التفاصيل", - "script_updating": "جاري تحديث السكربت {name}...", - "script_updated": "تم تحديث {name} إلى الإصدار {version}", - "script_update_failed": "فشل تحديث الوحدة. تحقق من السجلات لمزيد من التفاصيل", - "script_edit_failed": "فشل فتح ملف الوحدة. تحقق من السجلات لمزيد من التفاصيل", - "script_data_cleared": "تم مسح بيانات الوحدة!", - "script_data_clear_failed": "فشل مسح بيانات الوحدة. تحقق من السجلات لمزيد من التفاصيل", - "script_deleted": "تم حذف السكربت {name}!", - "script_delete_failed": "فشل حذف الوحدة. تحقق من السجلات لمزيد من التفاصيل", - "script_actions": "الإجراءات", - "script_no_description": "لا يوجد وصف", - "script_update_available": "التحديث متاح: {version}", - "script_loaded": "تم تحميل السكربت {name}", - "script_unloaded": "تم إلغاء تحميل السكربت {name}", - "script_enable_disable_failed": "فشل {action} السكربت. تحقق من السجلات لمزيد من التفاصيل", - "script_no_settings": "هذه الوحدة ليس لديها أي إعدادات", - "script_no_scripts_found": "لم يتم العثور على سكربتات", - "script_ok_timeout": "موافق {timeout}", - "scripting_tagline": "إدارة السكربتات، الاستيراد، والمجلدات", - "installed_scripts_tab": "المثبتة", - "catalog_tab": "الكتالوج", - "no_scripts_folder_selected_title": "حدد مجلد السكربتات للبدء", - "select_folder_button": "اختر المجلد", - "select_scripts_folder_toast": "يرجى اختيار مجلد السكربتات أولاً", - "delete_rule_title": "حذف القاعدة", - "delete_rule_description": "هل أنت متأكد أنك تريد حذف هذه القاعدة؟", - "rule_name": "اسم القاعدة", + "script_imported": "\u062a\u0645 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0633\u0643\u0631\u0628\u062a {name}!", + "script_import_failed": "\u0641\u0634\u0644 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0633\u0643\u0631\u0628\u062a. {error}. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644", + "script_updating": "\u062c\u0627\u0631\u064a \u062a\u062d\u062f\u064a\u062b \u0627\u0644\u0633\u0643\u0631\u0628\u062a {name}...", + "script_updated": "\u062a\u0645 \u062a\u062d\u062f\u064a\u062b {name} \u0625\u0644\u0649 \u0627\u0644\u0625\u0635\u062f\u0627\u0631 {version}", + "script_update_failed": "\u0641\u0634\u0644 \u062a\u062d\u062f\u064a\u062b \u0627\u0644\u0648\u062d\u062f\u0629. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644", + "script_edit_failed": "\u0641\u0634\u0644 \u0641\u062a\u062d \u0645\u0644\u0641 \u0627\u0644\u0648\u062d\u062f\u0629. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644", + "script_data_cleared": "\u062a\u0645 \u0645\u0633\u062d \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0648\u062d\u062f\u0629!", + "script_data_clear_failed": "\u0641\u0634\u0644 \u0645\u0633\u062d \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0648\u062d\u062f\u0629. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644", + "script_deleted": "\u062a\u0645 \u062d\u0630\u0641 \u0627\u0644\u0633\u0643\u0631\u0628\u062a {name}!", + "script_delete_failed": "\u0641\u0634\u0644 \u062d\u0630\u0641 \u0627\u0644\u0648\u062d\u062f\u0629. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644", + "script_actions": "\u0627\u0644\u0625\u062c\u0631\u0627\u0621\u0627\u062a", + "script_no_description": "\u0644\u0627 \u064a\u0648\u062c\u062f \u0648\u0635\u0641", + "script_update_available": "\u0627\u0644\u062a\u062d\u062f\u064a\u062b \u0645\u062a\u0627\u062d: {version}", + "script_loaded": "\u062a\u0645 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0633\u0643\u0631\u0628\u062a {name}", + "script_unloaded": "\u062a\u0645 \u0625\u0644\u063a\u0627\u0621 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0633\u0643\u0631\u0628\u062a {name}", + "script_enable_disable_failed": "\u0641\u0634\u0644 {action} \u0627\u0644\u0633\u0643\u0631\u0628\u062a. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644", + "script_no_settings": "\u0647\u0630\u0647 \u0627\u0644\u0648\u062d\u062f\u0629 \u0644\u064a\u0633 \u0644\u062f\u064a\u0647\u0627 \u0623\u064a \u0625\u0639\u062f\u0627\u062f\u0627\u062a", + "script_no_scripts_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0633\u0643\u0631\u0628\u062a\u0627\u062a", + "script_ok_timeout": "\u0645\u0648\u0627\u0641\u0642 {timeout}", + "scripting_tagline": "\u0625\u062f\u0627\u0631\u0629 \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a\u060c \u0627\u0644\u0627\u0633\u062a\u064a\u0631\u0627\u062f\u060c \u0648\u0627\u0644\u0645\u062c\u0644\u062f\u0627\u062a", + "installed_scripts_tab": "\u0627\u0644\u0645\u062b\u0628\u062a\u0629", + "catalog_tab": "\u0627\u0644\u0643\u062a\u0627\u0644\u0648\u062c", + "no_scripts_folder_selected_title": "\u062d\u062f\u062f \u0645\u062c\u0644\u062f \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a \u0644\u0644\u0628\u062f\u0621", + "select_folder_button": "\u0627\u062e\u062a\u0631 \u0627\u0644\u0645\u062c\u0644\u062f", + "select_scripts_folder_toast": "\u064a\u0631\u062c\u0649 \u0627\u062e\u062a\u064a\u0627\u0631 \u0645\u062c\u0644\u062f \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a \u0623\u0648\u0644\u0627\u064b", + "delete_rule_title": "\u062d\u0630\u0641 \u0627\u0644\u0642\u0627\u0639\u062f\u0629", + "delete_rule_description": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u062d\u0630\u0641 \u0647\u0630\u0647 \u0627\u0644\u0642\u0627\u0639\u062f\u0629\u061f", + "rule_name": "\u0627\u0633\u0645 \u0627\u0644\u0642\u0627\u0639\u062f\u0629", "friend_tracker_notifications": { - "notification_channel_name": "متتبع الأصدقاء", - "notification_title": "نشاط الصديق", - "conversation_enter": "{friend} دخل {conversation}", - "conversation_exit": "{friend} غادر {conversation}", - "started_typing": "{friend} بدأ الكتابة في {conversation}", - "stopped_typing": "{friend} توقف عن الكتابة في {conversation}", - "started_speaking": "{friend} بدأ التحدث في {conversation}", - "stopped_speaking": "{friend} توقف عن التحدث في {conversation}", - "started_peeking": "{friend} بدأ التلصص في {conversation}", - "stopped_peeking": "{friend} توقف عن التلصص في {conversation}", - "message_read": "{friend} قرأ رسالة في {conversation}", - "message_deleted": "{friend} حذف رسالة في {conversation}", - "message_saved": "{friend} حفظ رسالة في {conversation}", - "message_unsaved": "{friend} ألغى حفظ رسالة في {conversation}", - "message_edited": "{friend} حرر رسالة في {conversation}", - "message_reaction_add": "{friend} أضاف تفاعلاً في {conversation}", - "message_reaction_remove": "{friend} أزال تفاعلاً في {conversation}", - "snap_opened": "{friend} فتح snap في {conversation}", - "snap_replayed": "{friend} أعاد تشغيل snap في {conversation}", - "snap_replayed_twice": "{friend} أعاد تشغيل snap مرتين في {conversation}", - "snap_screenshot": "{friend} أخذ لقطة شاشة في {conversation}", - "snap_screen_record": "{friend} سجل الشاشة في {conversation}", - "i_can_see_you": "{friend} نشاط في {conversation}: {details}" + "notification_channel_name": "\u0645\u062a\u062a\u0628\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "notification_title": "\u0646\u0634\u0627\u0637 \u0627\u0644\u0635\u062f\u064a\u0642", + "conversation_enter": "{friend} \u062f\u062e\u0644 {conversation}", + "conversation_exit": "{friend} \u063a\u0627\u062f\u0631 {conversation}", + "started_typing": "{friend} \u0628\u062f\u0623 \u0627\u0644\u0643\u062a\u0627\u0628\u0629 \u0641\u064a {conversation}", + "stopped_typing": "{friend} \u062a\u0648\u0642\u0641 \u0639\u0646 \u0627\u0644\u0643\u062a\u0627\u0628\u0629 \u0641\u064a {conversation}", + "started_speaking": "{friend} \u0628\u062f\u0623 \u0627\u0644\u062a\u062d\u062f\u062b \u0641\u064a {conversation}", + "stopped_speaking": "{friend} \u062a\u0648\u0642\u0641 \u0639\u0646 \u0627\u0644\u062a\u062d\u062f\u062b \u0641\u064a {conversation}", + "started_peeking": "{friend} \u0628\u062f\u0623 \u0627\u0644\u062a\u0644\u0635\u0635 \u0641\u064a {conversation}", + "stopped_peeking": "{friend} \u062a\u0648\u0642\u0641 \u0639\u0646 \u0627\u0644\u062a\u0644\u0635\u0635 \u0641\u064a {conversation}", + "message_read": "{friend} \u0642\u0631\u0623 \u0631\u0633\u0627\u0644\u0629 \u0641\u064a {conversation}", + "message_deleted": "{friend} \u062d\u0630\u0641 \u0631\u0633\u0627\u0644\u0629 \u0641\u064a {conversation}", + "message_saved": "{friend} \u062d\u0641\u0638 \u0631\u0633\u0627\u0644\u0629 \u0641\u064a {conversation}", + "message_unsaved": "{friend} \u0623\u0644\u063a\u0649 \u062d\u0641\u0638 \u0631\u0633\u0627\u0644\u0629 \u0641\u064a {conversation}", + "message_edited": "{friend} \u062d\u0631\u0631 \u0631\u0633\u0627\u0644\u0629 \u0641\u064a {conversation}", + "message_reaction_add": "{friend} \u0623\u0636\u0627\u0641 \u062a\u0641\u0627\u0639\u0644\u0627\u064b \u0641\u064a {conversation}", + "message_reaction_remove": "{friend} \u0623\u0632\u0627\u0644 \u062a\u0641\u0627\u0639\u0644\u0627\u064b \u0641\u064a {conversation}", + "snap_opened": "{friend} \u0641\u062a\u062d snap \u0641\u064a {conversation}", + "snap_replayed": "{friend} \u0623\u0639\u0627\u062f \u062a\u0634\u063a\u064a\u0644 snap \u0641\u064a {conversation}", + "snap_replayed_twice": "{friend} \u0623\u0639\u0627\u062f \u062a\u0634\u063a\u064a\u0644 snap \u0645\u0631\u062a\u064a\u0646 \u0641\u064a {conversation}", + "snap_screenshot": "{friend} \u0623\u062e\u0630 \u0644\u0642\u0637\u0629 \u0634\u0627\u0634\u0629 \u0641\u064a {conversation}", + "snap_screen_record": "{friend} \u0633\u062c\u0644 \u0627\u0644\u0634\u0627\u0634\u0629 \u0641\u064a {conversation}", + "i_can_see_you": "{friend} \u0646\u0634\u0627\u0637 \u0641\u064a {conversation}: {details}" }, "friend_mutation_observer": { - "notification_channel_name": "مراقب تغييرات الصديق", - "friend_removed": "{username} قام بإزالتك كصديق", - "birthday_removed": "{username} قام بإزالة عيد ميلاده ({birthday})", - "birthday_added": "{username} قام بإضافة عيد ميلاده ({birthday})", - "birthday_changed": "{username} قام بتغيير عيد ميلاده من {oldBirthday} إلى {newBirthday}", - "bitmoji_selfie_changed": "{username} قام بتغيير سيلفي Bitmoji الخاص به", - "bitmoji_avatar_changed": "{username} قام بتغيير أفاتار Bitmoji الخاص به", - "bitmoji_background_changed": "{username} قام بتغيير خلفية Bitmoji الخاصة به", - "bitmoji_scene_changed": "{username} قام بتغيير مشهد Bitmoji الخاص به" + "notification_channel_name": "\u0645\u0631\u0627\u0642\u0628 \u062a\u063a\u064a\u064a\u0631\u0627\u062a \u0627\u0644\u0635\u062f\u064a\u0642", + "friend_removed": "{username} \u0642\u0627\u0645 \u0628\u0625\u0632\u0627\u0644\u062a\u0643 \u0643\u0635\u062f\u064a\u0642", + "birthday_removed": "{username} \u0642\u0627\u0645 \u0628\u0625\u0632\u0627\u0644\u0629 \u0639\u064a\u062f \u0645\u064a\u0644\u0627\u062f\u0647 ({birthday})", + "birthday_added": "{username} \u0642\u0627\u0645 \u0628\u0625\u0636\u0627\u0641\u0629 \u0639\u064a\u062f \u0645\u064a\u0644\u0627\u062f\u0647 ({birthday})", + "birthday_changed": "{username} \u0642\u0627\u0645 \u0628\u062a\u063a\u064a\u064a\u0631 \u0639\u064a\u062f \u0645\u064a\u0644\u0627\u062f\u0647 \u0645\u0646 {oldBirthday} \u0625\u0644\u0649 {newBirthday}", + "bitmoji_selfie_changed": "{username} \u0642\u0627\u0645 \u0628\u062a\u063a\u064a\u064a\u0631 \u0633\u064a\u0644\u0641\u064a Bitmoji \u0627\u0644\u062e\u0627\u0635 \u0628\u0647", + "bitmoji_avatar_changed": "{username} \u0642\u0627\u0645 \u0628\u062a\u063a\u064a\u064a\u0631 \u0623\u0641\u0627\u062a\u0627\u0631 Bitmoji \u0627\u0644\u062e\u0627\u0635 \u0628\u0647", + "bitmoji_background_changed": "{username} \u0642\u0627\u0645 \u0628\u062a\u063a\u064a\u064a\u0631 \u062e\u0644\u0641\u064a\u0629 Bitmoji \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0647", + "bitmoji_scene_changed": "{username} \u0642\u0627\u0645 \u0628\u062a\u063a\u064a\u064a\u0631 \u0645\u0634\u0647\u062f Bitmoji \u0627\u0644\u062e\u0627\u0635 \u0628\u0647" }, "material3_strings": { - "date_range_picker_start_headline": "من", - "date_range_picker_end_headline": "إلى", - "date_range_picker_title": "حدد النطاق الزمني", - "date_picker_switch_to_calendar_mode": "تقويم", - "date_picker_switch_to_input_mode": "إدخال", - "date_range_picker_scroll_to_previous_month": "الشهر السابق", - "date_range_picker_scroll_to_next_month": "الشهر التالي", - "date_picker_today_description": "اليوم", - "date_range_picker_day_in_range": "محدد", - "date_input_invalid_for_pattern": "تاريخ غير صالح", - "date_input_invalid_year_range": "سنة غير صالحة", - "date_input_invalid_not_allowed": "تاريخ غير صالح", - "date_range_input_invalid_range_input": "نطاق زمني غير صالح" + "date_range_picker_start_headline": "\u0645\u0646", + "date_range_picker_end_headline": "\u0625\u0644\u0649", + "date_range_picker_title": "\u062d\u062f\u062f \u0627\u0644\u0646\u0637\u0627\u0642 \u0627\u0644\u0632\u0645\u0646\u064a", + "date_picker_switch_to_calendar_mode": "\u062a\u0642\u0648\u064a\u0645", + "date_picker_switch_to_input_mode": "\u0625\u062f\u062e\u0627\u0644", + "date_range_picker_scroll_to_previous_month": "\u0627\u0644\u0634\u0647\u0631 \u0627\u0644\u0633\u0627\u0628\u0642", + "date_range_picker_scroll_to_next_month": "\u0627\u0644\u0634\u0647\u0631 \u0627\u0644\u062a\u0627\u0644\u064a", + "date_picker_today_description": "\u0627\u0644\u064a\u0648\u0645", + "date_range_picker_day_in_range": "\u0645\u062d\u062f\u062f", + "date_input_invalid_for_pattern": "\u062a\u0627\u0631\u064a\u062e \u063a\u064a\u0631 \u0635\u0627\u0644\u062d", + "date_input_invalid_year_range": "\u0633\u0646\u0629 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d\u0629", + "date_input_invalid_not_allowed": "\u062a\u0627\u0631\u064a\u062e \u063a\u064a\u0631 \u0635\u0627\u0644\u062d", + "date_range_input_invalid_range_input": "\u0646\u0637\u0627\u0642 \u0632\u0645\u0646\u064a \u063a\u064a\u0631 \u0635\u0627\u0644\u062d" }, "send_override_dialog": { - "title": "إرسال الوسائط كـ", - "duration": "المدة: {duration}", - "saveable_snap_hint": "جعل Snap قابلاً للحفظ في الدردشة", - "unlimited_duration": "غير محدود", - "schedule": "جدولة", - "select_time": "اختر وقتاً", - "select": "تحديد", - "select_date_first": "يرجى تحديد تاريخ أولاً", - "invalid_time": "يرجى تحديد وقت في المستقبل" + "title": "\u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637 \u0643\u0640", + "duration": "\u0627\u0644\u0645\u062f\u0629: {duration}", + "saveable_snap_hint": "\u062c\u0639\u0644 Snap \u0642\u0627\u0628\u0644\u0627\u064b \u0644\u0644\u062d\u0641\u0638 \u0641\u064a \u0627\u0644\u062f\u0631\u062f\u0634\u0629", + "unlimited_duration": "\u063a\u064a\u0631 \u0645\u062d\u062f\u0648\u062f", + "schedule": "\u062c\u062f\u0648\u0644\u0629", + "select_time": "\u0627\u062e\u062a\u0631 \u0648\u0642\u062a\u0627\u064b", + "select": "\u062a\u062d\u062f\u064a\u062f", + "select_date_first": "\u064a\u0631\u062c\u0649 \u062a\u062d\u062f\u064a\u062f \u062a\u0627\u0631\u064a\u062e \u0623\u0648\u0644\u0627\u064b", + "invalid_time": "\u064a\u0631\u062c\u0649 \u062a\u062d\u062f\u064a\u062f \u0648\u0642\u062a \u0641\u064a \u0627\u0644\u0645\u0633\u062a\u0642\u0628\u0644" }, "spotlight_creator_info": { - "title": "معلومات المنشئ", - "close": "إغلاق", - "creator_info": "معلومات المنشئ", - "display_name": "اسم العرض", - "username": "اسم المستخدم", - "user_id": "معرف المستخدم", - "posted_on": "نشر في", - "loading_username": "جاري التحميل...", - "username_copied": "تم نسخ اسم المستخدم", - "user_id_copied": "تم نسخ معرف المستخدم", - "friend_status": "حالة الصداقة", - "mutual_friend": "صديق مشترك", - "following": "تتابعهم", - "friend_request_sent": "تم إرسال الطلب", - "friend_request_received": "تم استلام الطلب", - "blocked": "محظور", - "friend_removed": "تمت الإزالة" + "title": "\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u0645\u0646\u0634\u0626", + "close": "\u0625\u063a\u0644\u0627\u0642", + "creator_info": "\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u0645\u0646\u0634\u0626", + "display_name": "\u0627\u0633\u0645 \u0627\u0644\u0639\u0631\u0636", + "username": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "user_id": "\u0645\u0639\u0631\u0641 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "posted_on": "\u0646\u0634\u0631 \u0641\u064a", + "loading_username": "\u062c\u0627\u0631\u064a \u0627\u0644\u062a\u062d\u0645\u064a\u0644...", + "username_copied": "\u062a\u0645 \u0646\u0633\u062e \u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "user_id_copied": "\u062a\u0645 \u0646\u0633\u062e \u0645\u0639\u0631\u0641 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "friend_status": "\u062d\u0627\u0644\u0629 \u0627\u0644\u0635\u062f\u0627\u0642\u0629", + "mutual_friend": "\u0635\u062f\u064a\u0642 \u0645\u0634\u062a\u0631\u0643", + "following": "\u062a\u062a\u0627\u0628\u0639\u0647\u0645", + "friend_request_sent": "\u062a\u0645 \u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0637\u0644\u0628", + "friend_request_received": "\u062a\u0645 \u0627\u0633\u062a\u0644\u0627\u0645 \u0627\u0644\u0637\u0644\u0628", + "blocked": "\u0645\u062d\u0638\u0648\u0631", + "friend_removed": "\u062a\u0645\u062a \u0627\u0644\u0625\u0632\u0627\u0644\u0629" }, "auto_reply_messages": { "dialog": { - "add_message": "إضافة رسالة", - "edit_message": "تحرير رسالة", - "message_label": "الرسالة", - "no_messages": "لا توجد رسائل بعد. أضف رسالتك الأولى!", - "message_placeholder": "أدخل رسالة الرد التلقائي الخاصة بك..." + "add_message": "\u0625\u0636\u0627\u0641\u0629 \u0631\u0633\u0627\u0644\u0629", + "edit_message": "\u062a\u062d\u0631\u064a\u0631 \u0631\u0633\u0627\u0644\u0629", + "message_label": "\u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "no_messages": "\u0644\u0627 \u062a\u0648\u062c\u062f \u0631\u0633\u0627\u0626\u0644 \u0628\u0639\u062f. \u0623\u0636\u0641 \u0631\u0633\u0627\u0644\u062a\u0643 \u0627\u0644\u0623\u0648\u0644\u0649!", + "message_placeholder": "\u0623\u062f\u062e\u0644 \u0631\u0633\u0627\u0644\u0629 \u0627\u0644\u0631\u062f \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0643..." } }, "auto_delete_sent_messages": { - "countdown_toast": "سيتم حذف الرسالة خلال {time}", - "delete_success_toast": "تم حذف الرسالة بنجاح", - "delete_failed_toast": "فشل حذف الرسالة", - "queue_cleared_toast": "تم مسح قائمة انتظار الحذف التلقائي" + "countdown_toast": "\u0633\u064a\u062a\u0645 \u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u062e\u0644\u0627\u0644 {time}", + "delete_success_toast": "\u062a\u0645 \u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u0628\u0646\u062c\u0627\u062d", + "delete_failed_toast": "\u0641\u0634\u0644 \u062d\u0630\u0641 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "queue_cleared_toast": "\u062a\u0645 \u0645\u0633\u062d \u0642\u0627\u0626\u0645\u0629 \u0627\u0646\u062a\u0638\u0627\u0631 \u0627\u0644\u062d\u0630\u0641 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a" }, "translation_position": { - "above": "فوق", - "below": "تحت", - "inline": "مضمن" + "above": "\u0641\u0648\u0642", + "below": "\u062a\u062d\u062a", + "inline": "\u0645\u0636\u0645\u0646" }, "language_codes": { - "en": "الإنجليزية", - "es": "الإسبانية", - "fr": "الفرنسية", - "de": "الألمانية", - "it": "الإيطالية", - "pt": "البرتغالية", - "ru": "الروسية", - "ja": "اليابانية", - "ko": "الكورية", - "zh": "الصينية", - "ar": "العربية", - "hi": "الهندية", - "tr": "التركية", - "nl": "الهولندية", - "pl": "البولندية", - "sv": "السويدية", - "da": "الدانماركية", - "no": "النرويجية", - "fi": "الفنلندية", - "cs": "التشيكية", - "hu": "المجرية", - "ro": "الرومانية", - "bg": "البلغارية", - "hr": "الكرواتية", - "sk": "السلوفاكية", - "sl": "السلوفينية", - "et": "الإستونية", - "lv": "اللاتفية", - "lt": "اللتوانية", - "mt": "المالطية", - "ga": "الأيرلندية", - "cy": "الويلزية" + "en": "\u0627\u0644\u0625\u0646\u062c\u0644\u064a\u0632\u064a\u0629", + "es": "\u0627\u0644\u0625\u0633\u0628\u0627\u0646\u064a\u0629", + "fr": "\u0627\u0644\u0641\u0631\u0646\u0633\u064a\u0629", + "de": "\u0627\u0644\u0623\u0644\u0645\u0627\u0646\u064a\u0629", + "it": "\u0627\u0644\u0625\u064a\u0637\u0627\u0644\u064a\u0629", + "pt": "\u0627\u0644\u0628\u0631\u062a\u063a\u0627\u0644\u064a\u0629", + "ru": "\u0627\u0644\u0631\u0648\u0633\u064a\u0629", + "ja": "\u0627\u0644\u064a\u0627\u0628\u0627\u0646\u064a\u0629", + "ko": "\u0627\u0644\u0643\u0648\u0631\u064a\u0629", + "zh": "\u0627\u0644\u0635\u064a\u0646\u064a\u0629", + "ar": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", + "hi": "\u0627\u0644\u0647\u0646\u062f\u064a\u0629", + "tr": "\u0627\u0644\u062a\u0631\u0643\u064a\u0629", + "nl": "\u0627\u0644\u0647\u0648\u0644\u0646\u062f\u064a\u0629", + "pl": "\u0627\u0644\u0628\u0648\u0644\u0646\u062f\u064a\u0629", + "sv": "\u0627\u0644\u0633\u0648\u064a\u062f\u064a\u0629", + "da": "\u0627\u0644\u062f\u0627\u0646\u0645\u0627\u0631\u0643\u064a\u0629", + "no": "\u0627\u0644\u0646\u0631\u0648\u064a\u062c\u064a\u0629", + "fi": "\u0627\u0644\u0641\u0646\u0644\u0646\u062f\u064a\u0629", + "cs": "\u0627\u0644\u062a\u0634\u064a\u0643\u064a\u0629", + "hu": "\u0627\u0644\u0645\u062c\u0631\u064a\u0629", + "ro": "\u0627\u0644\u0631\u0648\u0645\u0627\u0646\u064a\u0629", + "bg": "\u0627\u0644\u0628\u0644\u063a\u0627\u0631\u064a\u0629", + "hr": "\u0627\u0644\u0643\u0631\u0648\u0627\u062a\u064a\u0629", + "sk": "\u0627\u0644\u0633\u0644\u0648\u0641\u0627\u0643\u064a\u0629", + "sl": "\u0627\u0644\u0633\u0644\u0648\u0641\u064a\u0646\u064a\u0629", + "et": "\u0627\u0644\u0625\u0633\u062a\u0648\u0646\u064a\u0629", + "lv": "\u0627\u0644\u0644\u0627\u062a\u0641\u064a\u0629", + "lt": "\u0627\u0644\u0644\u062a\u0648\u0627\u0646\u064a\u0629", + "mt": "\u0627\u0644\u0645\u0627\u0644\u0637\u064a\u0629", + "ga": "\u0627\u0644\u0623\u064a\u0631\u0644\u0646\u062f\u064a\u0629", + "cy": "\u0627\u0644\u0648\u064a\u0644\u0632\u064a\u0629" }, "tracker": { "tabs": { - "logs": "السجلات", - "rules": "القواعد" + "logs": "\u0627\u0644\u0633\u062c\u0644\u0627\u062a", + "rules": "\u0627\u0644\u0642\u0648\u0627\u0639\u062f" }, "actions": { - "export": "تصدير", - "delete": "حذف", - "add_rule": "إضافة قاعدة", - "save_rule": "حفظ القاعدة" + "export": "\u062a\u0635\u062f\u064a\u0631", + "delete": "\u062d\u0630\u0641", + "add_rule": "\u0625\u0636\u0627\u0641\u0629 \u0642\u0627\u0639\u062f\u0629", + "save_rule": "\u062d\u0641\u0638 \u0627\u0644\u0642\u0627\u0639\u062f\u0629" }, "messages": { - "no_logs_found": "لم يتم العثور على سجلات", - "no_rules_found": "لم يتم العثور على قواعد", - "no_events": "لا توجد أحداث" + "no_logs_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0633\u062c\u0644\u0627\u062a", + "no_rules_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0642\u0648\u0627\u0639\u062f", + "no_events": "\u0644\u0627 \u062a\u0648\u062c\u062f \u0623\u062d\u062f\u0627\u062b" }, - "scopes_suffix": "نطاقات", + "scopes_suffix": "\u0646\u0637\u0627\u0642\u0627\u062a", "search": { - "placeholder": "بحث" + "placeholder": "\u0628\u062d\u062b" }, "filters": { - "newest_first": "الأحدث أولاً", - "pick_a_date": "اختر تاريخاً", - "title": "الفلاتر", - "search_by": "البحث بواسطة", - "since": "منذ", - "until": "حتى", + "newest_first": "\u0627\u0644\u0623\u062d\u062f\u062b \u0623\u0648\u0644\u0627\u064b", + "pick_a_date": "\u0627\u062e\u062a\u0631 \u062a\u0627\u0631\u064a\u062e\u0627\u064b", + "title": "\u0627\u0644\u0641\u0644\u0627\u062a\u0631", + "search_by": "\u0627\u0644\u0628\u062d\u062b \u0628\u0648\u0627\u0633\u0637\u0629", + "since": "\u0645\u0646\u0630", + "until": "\u062d\u062a\u0649", "types": { - "username": "اسم المستخدم", - "conversation": "المحادثة", - "event": "الحدث" + "username": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "conversation": "\u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "event": "\u0627\u0644\u062d\u062f\u062b" }, "event_types": { - "conversation_enter": "دخل المحادثة", - "conversation_exit": "غادر المحادثة", - "started_typing": "بدأ الكتابة", - "stopped_typing": "توقف عن الكتابة", - "started_speaking": "بدأ التحدث", - "stopped_speaking": "توقف عن التحدث", - "started_peeking": "بدأ التلصص", - "stopped_peeking": "توقف عن التلصص", - "message_read": "قرأ رسالة", - "message_deleted": "حذف رسالة", - "message_saved": "حفظ رسالة", - "message_unsaved": "ألغى حفظ رسالة", - "message_edited": "حرر رسالة", - "message_reaction_add": "أضاف تفاعلاً", - "message_reaction_remove": "أزال تفاعلاً", - "snap_opened": "فتح snap", - "snap_replayed": "أعاد تشغيل snap", - "snap_replayed_twice": "أعاد تشغيل snap مرتين", - "snap_screenshot": "أخذ لقطة شاشة", - "snap_screen_record": "سجل الشاشة" + "conversation_enter": "\u062f\u062e\u0644 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "conversation_exit": "\u063a\u0627\u062f\u0631 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "started_typing": "\u0628\u062f\u0623 \u0627\u0644\u0643\u062a\u0627\u0628\u0629", + "stopped_typing": "\u062a\u0648\u0642\u0641 \u0639\u0646 \u0627\u0644\u0643\u062a\u0627\u0628\u0629", + "started_speaking": "\u0628\u062f\u0623 \u0627\u0644\u062a\u062d\u062f\u062b", + "stopped_speaking": "\u062a\u0648\u0642\u0641 \u0639\u0646 \u0627\u0644\u062a\u062d\u062f\u062b", + "started_peeking": "\u0628\u062f\u0623 \u0627\u0644\u062a\u0644\u0635\u0635", + "stopped_peeking": "\u062a\u0648\u0642\u0641 \u0639\u0646 \u0627\u0644\u062a\u0644\u0635\u0635", + "message_read": "\u0642\u0631\u0623 \u0631\u0633\u0627\u0644\u0629", + "message_deleted": "\u062d\u0630\u0641 \u0631\u0633\u0627\u0644\u0629", + "message_saved": "\u062d\u0641\u0638 \u0631\u0633\u0627\u0644\u0629", + "message_unsaved": "\u0623\u0644\u063a\u0649 \u062d\u0641\u0638 \u0631\u0633\u0627\u0644\u0629", + "message_edited": "\u062d\u0631\u0631 \u0631\u0633\u0627\u0644\u0629", + "message_reaction_add": "\u0623\u0636\u0627\u0641 \u062a\u0641\u0627\u0639\u0644\u0627\u064b", + "message_reaction_remove": "\u0623\u0632\u0627\u0644 \u062a\u0641\u0627\u0639\u0644\u0627\u064b", + "snap_opened": "\u0641\u062a\u062d snap", + "snap_replayed": "\u0623\u0639\u0627\u062f \u062a\u0634\u063a\u064a\u0644 snap", + "snap_replayed_twice": "\u0623\u0639\u0627\u062f \u062a\u0634\u063a\u064a\u0644 snap \u0645\u0631\u062a\u064a\u0646", + "snap_screenshot": "\u0623\u062e\u0630 \u0644\u0642\u0637\u0629 \u0634\u0627\u0634\u0629", + "snap_screen_record": "\u0633\u062c\u0644 \u0627\u0644\u0634\u0627\u0634\u0629" } }, "logs": { "export_dialog": { - "title": "تصدير السجلات", - "description": "تصدير سجلات متتبع الأصدقاء إلى ملف", - "progress": "جاري تصدير السجلات...", - "export_as": "تصدير كـ {type}", + "title": "\u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0633\u062c\u0644\u0627\u062a", + "description": "\u062a\u0635\u062f\u064a\u0631 \u0633\u062c\u0644\u0627\u062a \u0645\u062a\u062a\u0628\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0625\u0644\u0649 \u0645\u0644\u0641", + "progress": "\u062c\u0627\u0631\u064a \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0633\u062c\u0644\u0627\u062a...", + "export_as": "\u062a\u0635\u062f\u064a\u0631 \u0643\u0640 {type}", "format_json": "JSON", "format_csv": "CSV" }, "delete_dialog": { - "title": "حذف السجلات", - "message": "هل أنت متأكد أنك تريد حذف جميع السجلات؟ هذا الإجراء لا يمكن التراجع عنه.", - "confirm": "حذف الكل", - "cancel": "إلغاء", - "progress": "جاري حذف {count} سجل..." + "title": "\u062d\u0630\u0641 \u0627\u0644\u0633\u062c\u0644\u0627\u062a", + "message": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u062d\u0630\u0641 \u062c\u0645\u064a\u0639 \u0627\u0644\u0633\u062c\u0644\u0627\u062a\u061f \u0647\u0630\u0627 \u0627\u0644\u0625\u062c\u0631\u0627\u0621 \u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u062a\u0631\u0627\u062c\u0639 \u0639\u0646\u0647.", + "confirm": "\u062d\u0630\u0641 \u0627\u0644\u0643\u0644", + "cancel": "\u0625\u0644\u063a\u0627\u0621", + "progress": "\u062c\u0627\u0631\u064a \u062d\u0630\u0641 {count} \u0633\u062c\u0644..." }, "log_entry": { - "in_conversation": "في {conversation}", - "unknown_user": "غير معروف", + "in_conversation": "\u0641\u064a {conversation}", + "unknown_user": "\u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641", "unknown_conversation": "DMs", - "i_can_see_you_entered": "دخل", - "i_can_see_you_left": "غادر", - "i_can_see_you_duration": "المدة", - "i_can_see_you_not_available": "غير متاح", - "i_can_see_you_unit_hour": "س", - "i_can_see_you_unit_minute": "د", - "i_can_see_you_unit_second": "ث", - "event_text": "{friend} {event} في {conversation}", + "i_can_see_you_entered": "\u062f\u062e\u0644", + "i_can_see_you_left": "\u063a\u0627\u062f\u0631", + "i_can_see_you_duration": "\u0627\u0644\u0645\u062f\u0629", + "i_can_see_you_not_available": "\u063a\u064a\u0631 \u0645\u062a\u0627\u062d", + "i_can_see_you_unit_hour": "\u0633", + "i_can_see_you_unit_minute": "\u062f", + "i_can_see_you_unit_second": "\u062b", + "event_text": "{friend} {event} \u0641\u064a {conversation}", "events": { - "conversation_enter": "دخل", - "conversation_exit": "غادر", - "started_typing": "بدأ الكتابة", - "stopped_typing": "توقف عن الكتابة", - "started_speaking": "بدأ التحدث", - "stopped_speaking": "توقف عن التحدث", - "started_peeking": "بدأ التلصص", - "stopped_peeking": "توقف عن التلصص", - "message_read": "قرأ رسالة", - "message_deleted": "حذف رسالة", - "message_saved": "حفظ رسالة", - "message_unsaved": "ألغى حفظ رسالة", - "message_edited": "حرر رسالة", - "message_reaction_add": "أضاف تفاعلاً", - "message_reaction_remove": "أزال تفاعلاً", - "snap_opened": "فتح snap", - "snap_replayed": "أعاد تشغيل snap", - "snap_replayed_twice": "أعاد تشغيل snap مرتين", - "snap_screenshot": "أخذ لقطة شاشة", - "snap_screen_record": "سجل الشاشة", - "i_can_see_you": "كان نشطاً" + "conversation_enter": "\u062f\u062e\u0644", + "conversation_exit": "\u063a\u0627\u062f\u0631", + "started_typing": "\u0628\u062f\u0623 \u0627\u0644\u0643\u062a\u0627\u0628\u0629", + "stopped_typing": "\u062a\u0648\u0642\u0641 \u0639\u0646 \u0627\u0644\u0643\u062a\u0627\u0628\u0629", + "started_speaking": "\u0628\u062f\u0623 \u0627\u0644\u062a\u062d\u062f\u062b", + "stopped_speaking": "\u062a\u0648\u0642\u0641 \u0639\u0646 \u0627\u0644\u062a\u062d\u062f\u062b", + "started_peeking": "\u0628\u062f\u0623 \u0627\u0644\u062a\u0644\u0635\u0635", + "stopped_peeking": "\u062a\u0648\u0642\u0641 \u0639\u0646 \u0627\u0644\u062a\u0644\u0635\u0635", + "message_read": "\u0642\u0631\u0623 \u0631\u0633\u0627\u0644\u0629", + "message_deleted": "\u062d\u0630\u0641 \u0631\u0633\u0627\u0644\u0629", + "message_saved": "\u062d\u0641\u0638 \u0631\u0633\u0627\u0644\u0629", + "message_unsaved": "\u0623\u0644\u063a\u0649 \u062d\u0641\u0638 \u0631\u0633\u0627\u0644\u0629", + "message_edited": "\u062d\u0631\u0631 \u0631\u0633\u0627\u0644\u0629", + "message_reaction_add": "\u0623\u0636\u0627\u0641 \u062a\u0641\u0627\u0639\u0644\u0627\u064b", + "message_reaction_remove": "\u0623\u0632\u0627\u0644 \u062a\u0641\u0627\u0639\u0644\u0627\u064b", + "snap_opened": "\u0641\u062a\u062d snap", + "snap_replayed": "\u0623\u0639\u0627\u062f \u062a\u0634\u063a\u064a\u0644 snap", + "snap_replayed_twice": "\u0623\u0639\u0627\u062f \u062a\u0634\u063a\u064a\u0644 snap \u0645\u0631\u062a\u064a\u0646", + "snap_screenshot": "\u0623\u062e\u0630 \u0644\u0642\u0637\u0629 \u0634\u0627\u0634\u0629", + "snap_screen_record": "\u0633\u062c\u0644 \u0627\u0644\u0634\u0627\u0634\u0629", + "i_can_see_you": "\u0643\u0627\u0646 \u0646\u0634\u0637\u0627\u064b" } } }, "edit_rule": { - "custom_rule": "قاعدة مخصصة", - "scope": "النطاق", - "events": "الأحداث", - "add_event": "إضافة حدث", - "type": "النوع", - "triggers": "المحفزات", - "conditions": "الشروط", - "only_inside_conversation": "فقط عندما أكون داخل المحادثة", - "only_outside_conversation": "فقط عندما أكون خارج المحادثة", - "only_when_app_active": "فقط عندما يكون Snapchat نشطاً", - "only_when_app_inactive": "فقط عندما يكون Snapchat غير نشط", - "no_notification_when_app_active": "لا يوجد إشعار عندما يكون Snapchat نشطاً", + "custom_rule": "\u0642\u0627\u0639\u062f\u0629 \u0645\u062e\u0635\u0635\u0629", + "scope": "\u0627\u0644\u0646\u0637\u0627\u0642", + "events": "\u0627\u0644\u0623\u062d\u062f\u0627\u062b", + "add_event": "\u0625\u0636\u0627\u0641\u0629 \u062d\u062f\u062b", + "type": "\u0627\u0644\u0646\u0648\u0639", + "triggers": "\u0627\u0644\u0645\u062d\u0641\u0632\u0627\u062a", + "conditions": "\u0627\u0644\u0634\u0631\u0648\u0637", + "only_inside_conversation": "\u0641\u0642\u0637 \u0639\u0646\u062f\u0645\u0627 \u0623\u0643\u0648\u0646 \u062f\u0627\u062e\u0644 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "only_outside_conversation": "\u0641\u0642\u0637 \u0639\u0646\u062f\u0645\u0627 \u0623\u0643\u0648\u0646 \u062e\u0627\u0631\u062c \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "only_when_app_active": "\u0641\u0642\u0637 \u0639\u0646\u062f\u0645\u0627 \u064a\u0643\u0648\u0646 Snapchat \u0646\u0634\u0637\u0627\u064b", + "only_when_app_inactive": "\u0641\u0642\u0637 \u0639\u0646\u062f\u0645\u0627 \u064a\u0643\u0648\u0646 Snapchat \u063a\u064a\u0631 \u0646\u0634\u0637", + "no_notification_when_app_active": "\u0644\u0627 \u064a\u0648\u062c\u062f \u0625\u0634\u0639\u0627\u0631 \u0639\u0646\u062f\u0645\u0627 \u064a\u0643\u0648\u0646 Snapchat \u0646\u0634\u0637\u0627\u064b", "scope_options": { - "all_friends_groups": "جميع الأصدقاء/المجموعات", - "no_one_except": "لا أحد باستثناء", - "everyone_except": "الجميع باستثناء" + "all_friends_groups": "\u062c\u0645\u064a\u0639 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621/\u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0627\u062a", + "no_one_except": "\u0644\u0627 \u0623\u062d\u062f \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621", + "everyone_except": "\u0627\u0644\u062c\u0645\u064a\u0639 \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621" } } }, "debug": { - "title": "تصحيح الأخطاء", - "clear": "مسح", + "title": "\u062a\u0635\u062d\u064a\u062d \u0627\u0644\u0623\u062e\u0637\u0627\u0621", + "clear": "\u0645\u0633\u062d", "files": { - "config_json": "ملف التكوين", - "mappings_json": "ملف التعيينات (Mappings)", - "message_logger_db": "قاعدة بيانات مسجل الرسائل", - "pinned_best_friend_txt": "ملف أفضل صديق مثبت", - "native_sig_cache_txt": "ملف ذاكرة التخزين المؤقت للتوقيع الأصلي" + "config_json": "\u0645\u0644\u0641 \u0627\u0644\u062a\u0643\u0648\u064a\u0646", + "mappings_json": "\u0645\u0644\u0641 \u0627\u0644\u062a\u0639\u064a\u064a\u0646\u0627\u062a (Mappings)", + "message_logger_db": "\u0642\u0627\u0639\u062f\u0629 \u0628\u064a\u0627\u0646\u0627\u062a \u0645\u0633\u062c\u0644 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "pinned_best_friend_txt": "\u0645\u0644\u0641 \u0623\u0641\u0636\u0644 \u0635\u062f\u064a\u0642 \u0645\u062b\u0628\u062a", + "native_sig_cache_txt": "\u0645\u0644\u0641 \u0630\u0627\u0643\u0631\u0629 \u0627\u0644\u062a\u062e\u0632\u064a\u0646 \u0627\u0644\u0645\u0624\u0642\u062a \u0644\u0644\u062a\u0648\u0642\u064a\u0639 \u0627\u0644\u0623\u0635\u0644\u064a" }, "settings": { - "test_mode": "وضع الاختبار (للتصحيح فقط)", - "disable_feature_loading": "تعطيل تحميل الميزات", - "disable_auto_mapper": "تعطيل المعين التلقائي", - "disable_bypass_status_indicator": "تعطيل مؤشر حالة التجاوز" + "test_mode": "\u0648\u0636\u0639 \u0627\u0644\u0627\u062e\u062a\u0628\u0627\u0631 (\u0644\u0644\u062a\u0635\u062d\u064a\u062d \u0641\u0642\u0637)", + "disable_feature_loading": "\u062a\u0639\u0637\u064a\u0644 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u064a\u0632\u0627\u062a", + "disable_auto_mapper": "\u062a\u0639\u0637\u064a\u0644 \u0627\u0644\u0645\u0639\u064a\u0646 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "disable_bypass_status_indicator": "\u062a\u0639\u0637\u064a\u0644 \u0645\u0624\u0634\u0631 \u062d\u0627\u0644\u0629 \u0627\u0644\u062a\u062c\u0627\u0648\u0632" } }, - "ui_settings_title": "إعدادات واجهة المستخدم", - "haptic_feedback_label": "الاستجابة اللمسية", - "updates_title": "التحديثات", - "auto_update_check": "فحص التحديث التلقائي", - "update_check_frequency_daily": "يومياً", - "update_check_frequency_weekly": "أسبوعياً", - "update_check_frequency_monthly": "شهرياً", - "update_channel_stable": "مستقر", - "update_channel_prerelease": "ما قبل الإصدار", - "friend_notes_title": "ملاحظات الأصدقاء", - "friend_notes_description": "إدارة ونسخ ملاحظات الأصدقاء احتياطياً", - "app_theme_title": "سمة التطبيق", - "theme_mode_system": "النظام", - "theme_mode_light": "فاتح", - "theme_mode_dark": "داكن", - "test_mode_label": "تمكين PurrAura", - "disable_feature_loading_label": "تعطيل تحميل الميزات", - "disable_auto_mapper_label": "تعطيل المعين التلقائي", - "disable_bypass_indicator_label": "تعطيل مؤشر التجاوز", + "ui_settings_title": "\u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0648\u0627\u062c\u0647\u0629 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645", + "haptic_feedback_label": "\u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0627\u0644\u0644\u0645\u0633\u064a\u0629", + "updates_title": "\u0627\u0644\u062a\u062d\u062f\u064a\u062b\u0627\u062a", + "auto_update_check": "\u0641\u062d\u0635 \u0627\u0644\u062a\u062d\u062f\u064a\u062b \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "update_check_frequency_daily": "\u064a\u0648\u0645\u064a\u0627\u064b", + "update_check_frequency_weekly": "\u0623\u0633\u0628\u0648\u0639\u064a\u0627\u064b", + "update_check_frequency_monthly": "\u0634\u0647\u0631\u064a\u0627\u064b", + "update_channel_stable": "\u0645\u0633\u062a\u0642\u0631", + "update_channel_prerelease": "\u0645\u0627 \u0642\u0628\u0644 \u0627\u0644\u0625\u0635\u062f\u0627\u0631", + "friend_notes_title": "\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "friend_notes_description": "\u0625\u062f\u0627\u0631\u0629 \u0648\u0646\u0633\u062e \u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0627\u062d\u062a\u064a\u0627\u0637\u064a\u0627\u064b", + "app_theme_title": "\u0633\u0645\u0629 \u0627\u0644\u062a\u0637\u0628\u064a\u0642", + "theme_mode_system": "\u0627\u0644\u0646\u0638\u0627\u0645", + "theme_mode_light": "\u0641\u0627\u062a\u062d", + "theme_mode_dark": "\u062f\u0627\u0643\u0646", + "test_mode_label": "\u062a\u0645\u0643\u064a\u0646 PurrAura", + "disable_feature_loading_label": "\u062a\u0639\u0637\u064a\u0644 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u064a\u0632\u0627\u062a", + "disable_auto_mapper_label": "\u062a\u0639\u0637\u064a\u0644 \u0627\u0644\u0645\u0639\u064a\u0646 \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a", + "disable_bypass_indicator_label": "\u062a\u0639\u0637\u064a\u0644 \u0645\u0624\u0634\u0631 \u0627\u0644\u062a\u062c\u0627\u0648\u0632", "friend_list": { - "manage_title": "إدارة قائمة الأصدقاء", - "export_description": "يسمح لك تصدير الأصدقاء بحفظ قائمة معرفات أصدقائك في ملف نصي. سيعرض الاستيراد من ملف الأصدقاء في قائمة حيث يمكنك إضافتهم.", - "export_friends": "تصدير الأصدقاء", - "import_from_file": "استيراد من ملف", - "load_suggested_friends": "تحميل الأصدقاء المقترحين", - "add": "إضافة" + "manage_title": "\u0625\u062f\u0627\u0631\u0629 \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "export_description": "\u064a\u0633\u0645\u062d \u0644\u0643 \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0628\u062d\u0641\u0638 \u0642\u0627\u0626\u0645\u0629 \u0645\u0639\u0631\u0641\u0627\u062a \u0623\u0635\u062f\u0642\u0627\u0626\u0643 \u0641\u064a \u0645\u0644\u0641 \u0646\u0635\u064a. \u0633\u064a\u0639\u0631\u0636 \u0627\u0644\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0645\u0646 \u0645\u0644\u0641 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0641\u064a \u0642\u0627\u0626\u0645\u0629 \u062d\u064a\u062b \u064a\u0645\u0643\u0646\u0643 \u0625\u0636\u0627\u0641\u062a\u0647\u0645.", + "export_friends": "\u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "import_from_file": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0645\u0646 \u0645\u0644\u0641", + "load_suggested_friends": "\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621 \u0627\u0644\u0645\u0642\u062a\u0631\u062d\u064a\u0646", + "add": "\u0625\u0636\u0627\u0641\u0629" }, "memories": { - "export_title": "تصدير الذكريات", - "total_memories": "إجمالي الذكريات: {count}", - "date_range": "النطاق الزمني", - "select": "تحديد", - "sort_by_folder": "فرز حسب المجلد", - "include_my_eyes_only": "تضمين عيني فقط (My Eyes Only)", - "cancel": "إلغاء", - "export": "تصدير", - "quit": "خروج", - "done": "تم", - "ok": "موافق", - "exporting_memories": "جاري تصدير الذكريات... ({failed} فشل)" + "export_title": "\u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0630\u0643\u0631\u064a\u0627\u062a", + "total_memories": "\u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0630\u0643\u0631\u064a\u0627\u062a: {count}", + "date_range": "\u0627\u0644\u0646\u0637\u0627\u0642 \u0627\u0644\u0632\u0645\u0646\u064a", + "select": "\u062a\u062d\u062f\u064a\u062f", + "sort_by_folder": "\u0641\u0631\u0632 \u062d\u0633\u0628 \u0627\u0644\u0645\u062c\u0644\u062f", + "include_my_eyes_only": "\u062a\u0636\u0645\u064a\u0646 \u0639\u064a\u0646\u064a \u0641\u0642\u0637 (My Eyes Only)", + "cancel": "\u0625\u0644\u063a\u0627\u0621", + "export": "\u062a\u0635\u062f\u064a\u0631", + "quit": "\u062e\u0631\u0648\u062c", + "done": "\u062a\u0645", + "ok": "\u0645\u0648\u0627\u0641\u0642", + "exporting_memories": "\u062c\u0627\u0631\u064a \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0630\u0643\u0631\u064a\u0627\u062a... ({failed} \u0641\u0634\u0644)" }, "scripting_ui": { - "no_scripts_folder_selected": "لم يتم تحديد مجلد السكربتات", - "select_folder": "تحديد المجلد", - "import_from_url": "استيراد من رابط", - "open_scripts_folder": "فتح مجلد السكربتات", - "import_script_from_url": "استيراد سكربت من رابط", - "warning_imported_scripts": "تحذير: يمكن أن تكون السكربتات المستوردة ضارة بجهازك. قم باستيراد السكربتات فقط من مصادر موثوقة.", - "enter_url_here": "أدخل الرابط هنا:", - "import": "استيراد", - "cancel": "إلغاء", - "documentation": "وثائق" + "no_scripts_folder_selected": "\u0644\u0645 \u064a\u062a\u0645 \u062a\u062d\u062f\u064a\u062f \u0645\u062c\u0644\u062f \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a", + "select_folder": "\u062a\u062d\u062f\u064a\u062f \u0627\u0644\u0645\u062c\u0644\u062f", + "import_from_url": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0645\u0646 \u0631\u0627\u0628\u0637", + "open_scripts_folder": "\u0641\u062a\u062d \u0645\u062c\u0644\u062f \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a", + "import_script_from_url": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0633\u0643\u0631\u0628\u062a \u0645\u0646 \u0631\u0627\u0628\u0637", + "warning_imported_scripts": "\u062a\u062d\u0630\u064a\u0631: \u064a\u0645\u0643\u0646 \u0623\u0646 \u062a\u0643\u0648\u0646 \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a \u0627\u0644\u0645\u0633\u062a\u0648\u0631\u062f\u0629 \u0636\u0627\u0631\u0629 \u0628\u062c\u0647\u0627\u0632\u0643. \u0642\u0645 \u0628\u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0633\u0643\u0631\u0628\u062a\u0627\u062a \u0641\u0642\u0637 \u0645\u0646 \u0645\u0635\u0627\u062f\u0631 \u0645\u0648\u062b\u0648\u0642\u0629.", + "enter_url_here": "\u0623\u062f\u062e\u0644 \u0627\u0644\u0631\u0627\u0628\u0637 \u0647\u0646\u0627:", + "import": "\u0627\u0633\u062a\u064a\u0631\u0627\u062f", + "cancel": "\u0625\u0644\u063a\u0627\u0621", + "documentation": "\u0648\u062b\u0627\u0626\u0642" }, "common": { - "cancel": "إلغاء", - "close": "إغلاق", - "add": "إضافة", - "ok": "موافق", - "quit": "خروج", - "done": "تم", - "back": "رجوع", - "unknown": "غير معروف", - "unknown_error": "خطأ غير معروف", - "not_available": "غير متاح", - "added": "مضاف", - "no_friends_found": "لم يتم العثور على أصدقاء", - "exporting_memories": "جاري تصدير الذكريات... ({failed} فشل)" + "cancel": "\u0625\u0644\u063a\u0627\u0621", + "close": "\u0625\u063a\u0644\u0627\u0642", + "add": "\u0625\u0636\u0627\u0641\u0629", + "ok": "\u0645\u0648\u0627\u0641\u0642", + "quit": "\u062e\u0631\u0648\u062c", + "done": "\u062a\u0645", + "back": "\u0631\u062c\u0648\u0639", + "unknown": "\u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641", + "unknown_error": "\u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641", + "not_available": "\u063a\u064a\u0631 \u0645\u062a\u0627\u062d", + "added": "\u0645\u0636\u0627\u0641", + "no_friends_found": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0623\u0635\u062f\u0642\u0627\u0621", + "exporting_memories": "\u062c\u0627\u0631\u064a \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0630\u0643\u0631\u064a\u0627\u062a... ({failed} \u0641\u0634\u0644)" }, - "clear_friend_feed": "مسح موجز الأصدقاء", - "task_media_conversion_title": "تحويل الوسائط", - "task_call_recording_title": "تسجيل المكالمة {author}", - "select_date": "حدد التاريخ", - "schedule_scheduled_for": "مجدول لـ {name} في {time}", - "schedule_sending_in": "إرسال خلال {time}", - "schedule_sent_to": "تم الإرسال إلى {name}", - "schedule_sent": "تم إرسال الـ snap المجدول", - "schedule_failed_to": "فشل الإرسال إلى {name}", - "schedule_failed": "فشل الـ snap المجدول", - "schedule_cancelled_for": "تم الإلغاء لـ {name}", - "by_author": "بواسطة {author}", - "version": "الإصدار {version}", - "delete_button": "حذف", + "clear_friend_feed": "\u0645\u0633\u062d \u0645\u0648\u062c\u0632 \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621", + "task_media_conversion_title": "\u062a\u062d\u0648\u064a\u0644 \u0627\u0644\u0648\u0633\u0627\u0626\u0637", + "task_call_recording_title": "\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0629 {author}", + "select_date": "\u062d\u062f\u062f \u0627\u0644\u062a\u0627\u0631\u064a\u062e", + "schedule_scheduled_for": "\u0645\u062c\u062f\u0648\u0644 \u0644\u0640 {name} \u0641\u064a {time}", + "schedule_sending_in": "\u0625\u0631\u0633\u0627\u0644 \u062e\u0644\u0627\u0644 {time}", + "schedule_sent_to": "\u062a\u0645 \u0627\u0644\u0625\u0631\u0633\u0627\u0644 \u0625\u0644\u0649 {name}", + "schedule_sent": "\u062a\u0645 \u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0640 snap \u0627\u0644\u0645\u062c\u062f\u0648\u0644", + "schedule_failed_to": "\u0641\u0634\u0644 \u0627\u0644\u0625\u0631\u0633\u0627\u0644 \u0625\u0644\u0649 {name}", + "schedule_failed": "\u0641\u0634\u0644 \u0627\u0644\u0640 snap \u0627\u0644\u0645\u062c\u062f\u0648\u0644", + "schedule_cancelled_for": "\u062a\u0645 \u0627\u0644\u0625\u0644\u063a\u0627\u0621 \u0644\u0640 {name}", + "by_author": "\u0628\u0648\u0627\u0633\u0637\u0629 {author}", + "version": "\u0627\u0644\u0625\u0635\u062f\u0627\u0631 {version}", + "delete_button": "\u062d\u0630\u0641", "logger_history": { - "download_started": "بدأ التنزيل!", - "downloaded_to": "تم التنزيل إلى {path}", - "failed_to_download": "فشل التنزيل {message}", - "select_conversation": "حدد محادثة", - "select_conversation_placeholder": "حدد محادثة", - "edited_at": "تم التحرير في {date}", - "download_attachment_failed_toast": "فشل تنزيل المرفق", - "message_parse_failed": "فشل تحليل الرسالة", - "empty_message": "رسالة فارغة", - "no_more_messages": "لا مزيد من الرسائل", - "reverse_order_checkbox": "عكس الترتيب", - "view_logger_history_button": "عرض سجل المسجل", - "posted_at": "نشرت في {date}", - "created_at": "أنشئت في {date}", - "failed_to_open_file": "فشل فتح الملف. تحقق من السجلات لمزيد من المعلومات", - "failed_to_get_file": "فشل الحصول على الملف", - "download_button": "تنزيل", - "chat_attachment": "مرفق {index}", + "download_started": "\u0628\u062f\u0623 \u0627\u0644\u062a\u0646\u0632\u064a\u0644!", + "downloaded_to": "\u062a\u0645 \u0627\u0644\u062a\u0646\u0632\u064a\u0644 \u0625\u0644\u0649 {path}", + "failed_to_download": "\u0641\u0634\u0644 \u0627\u0644\u062a\u0646\u0632\u064a\u0644 {message}", + "select_conversation": "\u062d\u062f\u062f \u0645\u062d\u0627\u062f\u062b\u0629", + "select_conversation_placeholder": "\u062d\u062f\u062f \u0645\u062d\u0627\u062f\u062b\u0629", + "edited_at": "\u062a\u0645 \u0627\u0644\u062a\u062d\u0631\u064a\u0631 \u0641\u064a {date}", + "download_attachment_failed_toast": "\u0641\u0634\u0644 \u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0645\u0631\u0641\u0642", + "message_parse_failed": "\u0641\u0634\u0644 \u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0631\u0633\u0627\u0644\u0629", + "empty_message": "\u0631\u0633\u0627\u0644\u0629 \u0641\u0627\u0631\u063a\u0629", + "no_more_messages": "\u0644\u0627 \u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u0631\u0633\u0627\u0626\u0644", + "reverse_order_checkbox": "\u0639\u0643\u0633 \u0627\u0644\u062a\u0631\u062a\u064a\u0628", + "view_logger_history_button": "\u0639\u0631\u0636 \u0633\u062c\u0644 \u0627\u0644\u0645\u0633\u062c\u0644", + "posted_at": "\u0646\u0634\u0631\u062a \u0641\u064a {date}", + "created_at": "\u0623\u0646\u0634\u0626\u062a \u0641\u064a {date}", + "failed_to_open_file": "\u0641\u0634\u0644 \u0641\u062a\u062d \u0627\u0644\u0645\u0644\u0641. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u0645\u0639\u0644\u0648\u0645\u0627\u062a", + "failed_to_get_file": "\u0641\u0634\u0644 \u0627\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0644\u0641", + "download_button": "\u062a\u0646\u0632\u064a\u0644", + "chat_attachment": "\u0645\u0631\u0641\u0642 {index}", "log_header_format": "{username} ? {type} ? {date}", - "edited_at_text": "تم التحرير إلى \"{message}\" في {date}", - "list_group_format": "مجموعة {name}", - "list_friend_format": "صديق {name}", - "download_started_toast": "بدأ التنزيل", - "download_success_toast": "تم التنزيل إلى {path}", - "download_failed_toast": "فشل التنزيل: {message}", - "close_button_description": "إغلاق البحث", - "search_button_description": "بحث في الرسائل" + "edited_at_text": "\u062a\u0645 \u0627\u0644\u062a\u062d\u0631\u064a\u0631 \u0625\u0644\u0649 \"{message}\" \u0641\u064a {date}", + "list_group_format": "\u0645\u062c\u0645\u0648\u0639\u0629 {name}", + "list_friend_format": "\u0635\u062f\u064a\u0642 {name}", + "download_started_toast": "\u0628\u062f\u0623 \u0627\u0644\u062a\u0646\u0632\u064a\u0644", + "download_success_toast": "\u062a\u0645 \u0627\u0644\u062a\u0646\u0632\u064a\u0644 \u0625\u0644\u0649 {path}", + "download_failed_toast": "\u0641\u0634\u0644 \u0627\u0644\u062a\u0646\u0632\u064a\u0644: {message}", + "close_button_description": "\u0625\u063a\u0644\u0627\u0642 \u0627\u0644\u0628\u062d\u062b", + "search_button_description": "\u0628\u062d\u062b \u0641\u064a \u0627\u0644\u0631\u0633\u0627\u0626\u0644" }, "debug_dialogs": { - "info": "معلومات", - "refs": "المراجع", + "info": "\u0645\u0639\u0644\u0648\u0645\u0627\u062a", + "refs": "\u0627\u0644\u0645\u0631\u0627\u062c\u0639", "arroyo": "Arroyo", - "message": "رسالة", - "media_references": "مراجع الوسائط", + "message": "\u0631\u0633\u0627\u0644\u0629", + "media_references": "\u0645\u0631\u0627\u062c\u0639 \u0627\u0644\u0648\u0633\u0627\u0626\u0637", "arroyo_proto": "Arroyo proto", "message_proto": "Message proto" }, "error_messages": { - "failed_to_fetch_message": "فشل جلب الرسالة: {error}", - "failed_to_edit_message": "فشل تحرير الرسالة: {error}" + "failed_to_fetch_message": "\u0641\u0634\u0644 \u062c\u0644\u0628 \u0627\u0644\u0631\u0633\u0627\u0644\u0629: {error}", + "failed_to_edit_message": "\u0641\u0634\u0644 \u062a\u062d\u0631\u064a\u0631 \u0627\u0644\u0631\u0633\u0627\u0644\u0629: {error}" }, - "toast_snapchat_not_installed": "لا يمكن تنفيذ الإجراء: Snapchat غير مثبت", - "invalid_input_toast": "إدخال غير صالح! تأكد من إدخال قيمة صالحة.", - "toast_async_task_failed": "فشلت المهمة غير المتزامنة: {message}", - "toast_snapchat_crashed": "تعطل Snapchat! يرجى التحقق من السجلات لمزيد من التفاصيل.", - "toast_init_features_failed": "فشل تهيئة الميزات! قد لا تعمل بعض الوظائف بشكل صحيح.", - "toast_init_script_runtime_failed": "فشل تهيئة وقت تشغيل السكربت!", - "toast_database_corrupted": "قاعدة البيانات {path} تالفة! جاري إعادة التشغيل...", - "toast_feature_init_failed": "فشل تهيئة الميزة {feature}! تحقق من logcat لمزيد من التفاصيل.", - "toast_updating_purrfectsnap": "جاري تحديث PurrfectSnap. يرجى الانتظار...", - "toast_update_purrfectsnap_failed": "فشل تحديث PurrfectSnap. يرجى التحقق من logcat لمزيد من التفاصيل.", - "toast_purrfectsnap_updated": "تم تحديث PurrfectSnap!", - "toast_export_memories_failed": "فشل تصدير الذكريات", - "toast_exported_to_path": "تم التصدير إلى {path}", - "toast_open_memories_db_failed": "فشل فتح قاعدة بيانات الذكريات", - "toast_friend_add_unavailable": "فشل إضافة صديق: FriendRelationshipChanger غير متاح", - "toast_friend_add_failed": "فشل إضافة صديق: {message}", - "toast_friends_exported": "تم تصدير {count} أصدقاء!", - "toast_friends_import_failed": "فشل استيراد الأصدقاء: {message}", - "toast_translation_service_unavailable": "خدمة الترجمة غير متاحة مؤقتاً", - "toast_send_message_failed": "فشل إرسال الرسالة: {error}", - "toast_mark_conversation_read_failed": "فشل وضع علامة مقروء على المحادثة", - "toast_fetch_conversation_failed": "فشل جلب المحادثة", - "toast_open_snap_failed": "فشل فتح snap", - "toast_mark_message_read_failed": "فشل وضع علامة مقروء على الرسالة. تحقق من السجلات لمزيد من التفاصيل", - "toast_open_conversation_first": "يجب عليك فتح محادثة أولاً", + "toast_snapchat_not_installed": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u0646\u0641\u064a\u0630 \u0627\u0644\u0625\u062c\u0631\u0627\u0621: Snapchat \u063a\u064a\u0631 \u0645\u062b\u0628\u062a", + "invalid_input_toast": "\u0625\u062f\u062e\u0627\u0644 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d! \u062a\u0623\u0643\u062f \u0645\u0646 \u0625\u062f\u062e\u0627\u0644 \u0642\u064a\u0645\u0629 \u0635\u0627\u0644\u062d\u0629.", + "toast_async_task_failed": "\u0641\u0634\u0644\u062a \u0627\u0644\u0645\u0647\u0645\u0629 \u063a\u064a\u0631 \u0627\u0644\u0645\u062a\u0632\u0627\u0645\u0646\u0629: {message}", + "toast_snapchat_crashed": "\u062a\u0639\u0637\u0644 Snapchat! \u064a\u0631\u062c\u0649 \u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644.", + "toast_init_features_failed": "\u0641\u0634\u0644 \u062a\u0647\u064a\u0626\u0629 \u0627\u0644\u0645\u064a\u0632\u0627\u062a! \u0642\u062f \u0644\u0627 \u062a\u0639\u0645\u0644 \u0628\u0639\u0636 \u0627\u0644\u0648\u0638\u0627\u0626\u0641 \u0628\u0634\u0643\u0644 \u0635\u062d\u064a\u062d.", + "toast_init_script_runtime_failed": "\u0641\u0634\u0644 \u062a\u0647\u064a\u0626\u0629 \u0648\u0642\u062a \u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0633\u0643\u0631\u0628\u062a!", + "toast_database_corrupted": "\u0642\u0627\u0639\u062f\u0629 \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a {path} \u062a\u0627\u0644\u0641\u0629! \u062c\u0627\u0631\u064a \u0625\u0639\u0627\u062f\u0629 \u0627\u0644\u062a\u0634\u063a\u064a\u0644...", + "toast_feature_init_failed": "\u0641\u0634\u0644 \u062a\u0647\u064a\u0626\u0629 \u0627\u0644\u0645\u064a\u0632\u0629 {feature}! \u062a\u062d\u0642\u0642 \u0645\u0646 logcat \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644.", + "toast_updating_purrfectsnap": "\u062c\u0627\u0631\u064a \u062a\u062d\u062f\u064a\u062b PurrfectSnap. \u064a\u0631\u062c\u0649 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631...", + "toast_update_purrfectsnap_failed": "\u0641\u0634\u0644 \u062a\u062d\u062f\u064a\u062b PurrfectSnap. \u064a\u0631\u062c\u0649 \u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0646 logcat \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644.", + "toast_purrfectsnap_updated": "\u062a\u0645 \u062a\u062d\u062f\u064a\u062b PurrfectSnap!", + "toast_export_memories_failed": "\u0641\u0634\u0644 \u062a\u0635\u062f\u064a\u0631 \u0627\u0644\u0630\u0643\u0631\u064a\u0627\u062a", + "toast_exported_to_path": "\u062a\u0645 \u0627\u0644\u062a\u0635\u062f\u064a\u0631 \u0625\u0644\u0649 {path}", + "toast_open_memories_db_failed": "\u0641\u0634\u0644 \u0641\u062a\u062d \u0642\u0627\u0639\u062f\u0629 \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0630\u0643\u0631\u064a\u0627\u062a", + "toast_friend_add_unavailable": "\u0641\u0634\u0644 \u0625\u0636\u0627\u0641\u0629 \u0635\u062f\u064a\u0642: FriendRelationshipChanger \u063a\u064a\u0631 \u0645\u062a\u0627\u062d", + "toast_friend_add_failed": "\u0641\u0634\u0644 \u0625\u0636\u0627\u0641\u0629 \u0635\u062f\u064a\u0642: {message}", + "toast_friends_exported": "\u062a\u0645 \u062a\u0635\u062f\u064a\u0631 {count} \u0623\u0635\u062f\u0642\u0627\u0621!", + "toast_friends_import_failed": "\u0641\u0634\u0644 \u0627\u0633\u062a\u064a\u0631\u0627\u062f \u0627\u0644\u0623\u0635\u062f\u0642\u0627\u0621: {message}", + "toast_translation_service_unavailable": "\u062e\u062f\u0645\u0629 \u0627\u0644\u062a\u0631\u062c\u0645\u0629 \u063a\u064a\u0631 \u0645\u062a\u0627\u062d\u0629 \u0645\u0624\u0642\u062a\u0627\u064b", + "toast_send_message_failed": "\u0641\u0634\u0644 \u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0631\u0633\u0627\u0644\u0629: {error}", + "toast_mark_conversation_read_failed": "\u0641\u0634\u0644 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0645\u0642\u0631\u0648\u0621 \u0639\u0644\u0649 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "toast_fetch_conversation_failed": "\u0641\u0634\u0644 \u062c\u0644\u0628 \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "toast_open_snap_failed": "\u0641\u0634\u0644 \u0641\u062a\u062d snap", + "toast_mark_message_read_failed": "\u0641\u0634\u0644 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0645\u0642\u0631\u0648\u0621 \u0639\u0644\u0649 \u0627\u0644\u0631\u0633\u0627\u0644\u0629. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0633\u062c\u0644\u0627\u062a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u062a\u0641\u0627\u0635\u064a\u0644", + "toast_open_conversation_first": "\u064a\u062c\u0628 \u0639\u0644\u064a\u0643 \u0641\u062a\u062d \u0645\u062d\u0627\u062f\u062b\u0629 \u0623\u0648\u0644\u0627\u064b", "conversation_toolbox": { - "title": "صندوق أدوات المحادثة", - "loaded_script": "السكربت المحمل", - "failed_to_load": "فشل التحميل: {message}" + "title": "\u0635\u0646\u062f\u0648\u0642 \u0623\u062f\u0648\u0627\u062a \u0627\u0644\u0645\u062d\u0627\u062f\u062b\u0629", + "loaded_script": "\u0627\u0644\u0633\u0643\u0631\u0628\u062a \u0627\u0644\u0645\u062d\u0645\u0644", + "failed_to_load": "\u0641\u0634\u0644 \u0627\u0644\u062a\u062d\u0645\u064a\u0644: {message}" }, - "toast_open_link_failed": "فشل فتح الرابط", + "toast_open_link_failed": "\u0641\u0634\u0644 \u0641\u062a\u062d \u0627\u0644\u0631\u0627\u0628\u0637", "ai_response_style": { - "casual": "عفوي", - "formal": "رسمي", - "friendly": "ودود", - "humorous": "فكاهي", - "empathetic": "متعاطف", - "busy": "مشغول", - "toxic": "سام" + "casual": "\u0639\u0641\u0648\u064a", + "formal": "\u0631\u0633\u0645\u064a", + "friendly": "\u0648\u062f\u0648\u062f", + "humorous": "\u0641\u0643\u0627\u0647\u064a", + "empathetic": "\u0645\u062a\u0639\u0627\u0637\u0641", + "busy": "\u0645\u0634\u063a\u0648\u0644", + "toxic": "\u0633\u0627\u0645" }, "ai_response_language": { - "auto": "تلقائي (نفس المستلم)", - "en": "الإنجليزية", - "es": "الإسبانية", - "fr": "الفرنسية", - "de": "الألمانية", - "it": "الإيطالية", - "pt": "البرتغالية", - "ru": "الروسية", - "ja": "اليابانية", - "ko": "الكورية", - "zh": "الصينية", - "ar": "العربية (الإمارات) و (السعودية)", - "hi": "الهندية", - "tr": "التركية", - "pl": "البولندية", - "nl": "الهولندية", - "sv": "السويدية", - "da": "الدانماركية", - "no": "النرويجية", - "fi": "الفنلندية" + "auto": "\u062a\u0644\u0642\u0627\u0626\u064a (\u0646\u0641\u0633 \u0627\u0644\u0645\u0633\u062a\u0644\u0645)", + "en": "\u0627\u0644\u0625\u0646\u062c\u0644\u064a\u0632\u064a\u0629", + "es": "\u0627\u0644\u0625\u0633\u0628\u0627\u0646\u064a\u0629", + "fr": "\u0627\u0644\u0641\u0631\u0646\u0633\u064a\u0629", + "de": "\u0627\u0644\u0623\u0644\u0645\u0627\u0646\u064a\u0629", + "it": "\u0627\u0644\u0625\u064a\u0637\u0627\u0644\u064a\u0629", + "pt": "\u0627\u0644\u0628\u0631\u062a\u063a\u0627\u0644\u064a\u0629", + "ru": "\u0627\u0644\u0631\u0648\u0633\u064a\u0629", + "ja": "\u0627\u0644\u064a\u0627\u0628\u0627\u0646\u064a\u0629", + "ko": "\u0627\u0644\u0643\u0648\u0631\u064a\u0629", + "zh": "\u0627\u0644\u0635\u064a\u0646\u064a\u0629", + "ar": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (\u0627\u0644\u0625\u0645\u0627\u0631\u0627\u062a) \u0648 (\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629)", + "hi": "\u0627\u0644\u0647\u0646\u062f\u064a\u0629", + "tr": "\u0627\u0644\u062a\u0631\u0643\u064a\u0629", + "pl": "\u0627\u0644\u0628\u0648\u0644\u0646\u062f\u064a\u0629", + "nl": "\u0627\u0644\u0647\u0648\u0644\u0646\u062f\u064a\u0629", + "sv": "\u0627\u0644\u0633\u0648\u064a\u062f\u064a\u0629", + "da": "\u0627\u0644\u062f\u0627\u0646\u0645\u0627\u0631\u0643\u064a\u0629", + "no": "\u0627\u0644\u0646\u0631\u0648\u064a\u062c\u064a\u0629", + "fi": "\u0627\u0644\u0641\u0646\u0644\u0646\u062f\u064a\u0629" }, "ai_provider": { "gemini": "Gemini", diff --git a/common/src/main/assets/lang/en_UK.json b/common/src/main/assets/lang/en_UK.json index c1f595a7..c57d1486 100644 --- a/common/src/main/assets/lang/en_UK.json +++ b/common/src/main/assets/lang/en_UK.json @@ -270,6 +270,26 @@ "success_toast": "Done!", "message_logger_summary": "{messageCount} messages\n{storyCount} stories", "export_button": "Export", + "message_logger_export_title": "Export Message Logger", + "message_logger_export_text": "Choose what to export.", + "message_logger_export_individual_chat": "Export Individual Chat", + "message_logger_export_full_database": "Export Full Database", + "message_logger_select_chat_title": "Export Individual Chat", + "message_logger_select_chat_text": "Search by username, display name, or chat name.", + "message_logger_continue_button": "Continue", + "message_logger_select_export_format_title": "Select Export Format", + "message_logger_select_export_format_text": "Choose how to export the selected chat.", + "message_logger_export_format_db": ".db", + "message_logger_export_format_html": "HTML", + "message_logger_export_format_txt": "TXT", + "message_logger_no_chats_found": "No chats found", + "message_logger_no_messages_export_text": "No messages found in this chat.", + "message_logger_conversation_id": "Conversation ID: {id}", + "message_logger_message_count": "{count} messages", + "message_logger_missing_attachment_placeholder": "Attachment unavailable", + "message_logger_export_failed_toast": "Export failed: {message}", + "message_logger_missing_conversation_toast": "Missing conversation ID", + "message_logger_empty_chat_toast": "Selected chat has no messages to export", "import_button": "Import", "clear_button": "Clear", "view_logger_history_button": "View Logger History", @@ -1105,6 +1125,10 @@ "name": "Show Streak Expiration Info", "description": "Shows a Streak Expiration timer next to the Streaks counter" }, + "sort_social_tab_by_streak_length": { + "name": "Sort Social Tab by Streak Length", + "description": "Shows friends with streaks first, ordered from the longest streak to the shortest in the social tab and friend picker" + }, "hide_friend_feed_entry": { "name": "Hide Friend Feed Entry", "description": "Hides a specific friend from the Friend Feed\nUse the social tab to manage this feature" @@ -2314,6 +2338,8 @@ "unsaveable_messages": "\u2b07\ufe0f Unsaveable Messages", "auto_open_snaps": "\ud83d\udcf7 Auto Open Snaps", "stealth": "\ud83d\udc7b Full Stealth Mode", + "snap_stealth": "\ud83d\udcf7 Snap Stealth Mode", + "chat_stealth": "\ud83d\udcac Chat Stealth Mode", "auto_reply": "\ud83d\udce8 Auto Reply", "auto_delete_sent_messages": "\ud83d\uddd1\ufe0f Auto Delete Sent Messages", "mark_snaps_as_seen": "\ud83d\udc40 Mark Snaps as seen", @@ -2641,7 +2667,10 @@ "platform_indicator": "Adds the platform icon from which a media was sent (e.g. Android, iOS, Web)", "location_indicator": "Adds a \ud83d\udccd icon to snaps when they have been sent with location enabled", "ovf_editor_indicator": "Indicates if a snap has been sent using OVF Editor", - "director_mode_indicator": "Adds a \u270f\ufe0f icon to snaps when they have been sent using Director Mode, which can be used to send gallery images as snaps" + "director_mode_indicator": "Adds a \u270f\ufe0f icon to snaps when they have been sent using Director Mode, which can be used to send gallery images as snaps", + "memories_indicator": "Adds a \ud83d\udcd6 icon to snaps that were re-sent from Memories instead of being captured with the live camera", + "skip_own_indicators": "Hides indicator icons on your own sent snaps (Self-Snaps) \ud83d\udc64", + "disable_indicators_in_groups": "Disables all indicator icons in group conversations to reduce UI clutter \ud83d\udc65" }, "auto_mark_as_read": { "conversation_read": "Mark conversation as read when sending a message", diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 11aa1dcd..03642da5 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -197,12 +197,12 @@ }, "sections": { "home": { - "version_title": "v{versionName} \u00b7 by ΞTΞRNAL", + "version_title": "v{versionName} \u00b7 by \u039eT\u039eRNAL", "update_title": "PurrfectSnap Update", "update_content": "Version {version} is available!", "update_button": "Download", "hero_tagline": "An Xposed Module meant to enhance your Snapchat experience", - "hero_version_label": "Version: {version} - {channel}", + "hero_version_label": "Version: {version}", "hero_build_label": "Build: {build}", "update_ready_label": "Ready to install", "purr_aura_active_label": "PurrAura Active!", @@ -247,9 +247,9 @@ "about_tagline": "An Xposed Module meant to enhance your Snapchat experience!", "about_lead_developers_title": "Lead Developers", "about_story_title": "Our Story", - "about_story": "PurrfectSnap was founded on 2nd of October, 2025, as a fork of SnapEnhance by ΞTΞRNAL with a vision to provide users the quality Snapchat experience they deserve. This app was just meant to be a minor update in the SnapEnhance repository, but it soon became a separate app wherein the contributors kept adding features. Then the developer joined the team, and this app soon became a huge success.\n\nWe would also like to convey our huge thanks to rhunk, the lead developer of SnapEnhance, as without him, this app wouldn't even exist. We are immensely grateful to him.\n\nWe received much love and support and gained 1K+ downloads in just two days! We thank all users and contributors; without your support, we wouldn't have reached this place.\n\nLastly, we would like to thank all our admins, notably: CLASSIC GENIUS, Harry, Sujal, Zain & schrodingerspet, who were right there with us from the very beginning. We would also like to thank all testers, notably Leo & Toxic, who tested and reported bugs continuously. We are immensely grateful for your contribution.", + "about_story": "PurrfectSnap was founded on 2nd of October, 2025, as a fork of SnapEnhance by \u039eT\u039eRNAL with a vision to provide users the quality Snapchat experience they deserve. This app was just meant to be a minor update in the SnapEnhance repository, but it soon became a separate app wherein the contributors kept adding features. Then the developer joined the team, and this app soon became a huge success.\n\nWe would also like to convey our huge thanks to rhunk, the lead developer of SnapEnhance, as without him, this app wouldn't even exist. We are immensely grateful to him.\n\nWe received much love and support and gained 1K+ downloads in just two days! We thank all users and contributors; without your support, we wouldn't have reached this place.\n\nLastly, we would like to thank all our admins, notably: CLASSIC GENIUS, Harry, Sujal, Zain & schrodingerspet, who were right there with us from the very beginning. We would also like to thank all testers, notably Leo & Toxic, who tested and reported bugs continuously. We are immensely grateful for your contribution.", "about_thanks_title": "With love, PurrfectSnap Team", - "about_magic_toast": "Tap 5 times in this screen to see some magic 😉!", + "about_magic_toast": "Tap 5 times in this screen to see some magic \ud83d\ude09!", "github_button": "GitHub", "telegram_button": "Telegram" }, @@ -281,6 +281,26 @@ "success_toast": "Done!", "message_logger_summary": "{messageCount} messages\n{storyCount} stories", "export_button": "Export", + "message_logger_export_title": "Export Message Logger", + "message_logger_export_text": "Choose what to export.", + "message_logger_export_individual_chat": "Export Individual Chat", + "message_logger_export_full_database": "Export Full Database", + "message_logger_select_chat_title": "Export Individual Chat", + "message_logger_select_chat_text": "Search by username, display name, or chat name.", + "message_logger_continue_button": "Continue", + "message_logger_select_export_format_title": "Select Export Format", + "message_logger_select_export_format_text": "Choose how to export the selected chat.", + "message_logger_export_format_db": ".db", + "message_logger_export_format_html": "HTML", + "message_logger_export_format_txt": "TXT", + "message_logger_no_chats_found": "No chats found", + "message_logger_no_messages_export_text": "No messages found in this chat.", + "message_logger_conversation_id": "Conversation ID: {id}", + "message_logger_message_count": "{count} messages", + "message_logger_missing_attachment_placeholder": "Attachment unavailable", + "message_logger_export_failed_toast": "Export failed: {message}", + "message_logger_missing_conversation_toast": "Missing conversation ID", + "message_logger_empty_chat_toast": "Selected chat has no messages to export", "import_button": "Import", "clear_button": "Clear", "view_logger_history_button": "View Logger History", @@ -357,21 +377,22 @@ "remove_all_tasks_confirm": "Remove all tasks?" }, "features": { - "disabled": "Disabled", - "export_option": "Export", - "import_option": "Import", - "reset_option": "Reset", - "config_export_success_toast": "Config exported successfully", - "config_import_success_toast": "Config imported successfully", - "config_import_failure_toast": "Failed to import config {error}", - "config_export_failure_toast": "Failed to export config {error}", - "saved_config_snackbar": "Config saved", - "older_required": "This feature requires Snapchat v{version} or older to work correctly", - "newer_required": "This feature requires Snapchat v{version} or newer to work correctly", + "disabled": "Disabled", + "export_option": "Export", + "import_option": "Import", + "reset_option": "Reset", + "config_export_success_toast": "Config exported successfully", + "config_import_success_toast": "Config imported successfully", + "config_import_failure_toast": "Failed to import config {error}", + "config_export_failure_toast": "Failed to export config {error}", + "saved_config_snackbar": "Config saved", + "older_required": "This feature requires Snapchat v{version} or older to work correctly", + "newer_required": "This feature requires Snapchat v{version} or newer to work correctly", "search_button": "Search", "search_results_count": "{count} messages", "clear_history": "Clear search history", - "subtitle": "Explore and manage premium features" + "subtitle": "Explore and manage premium features", + "digits_only_toast": "Only numbers are allowed." }, "bypass_status": { "active": "PurrAura Active", @@ -1156,6 +1177,10 @@ "name": "Show Streak Expiration Info", "description": "Shows a Streak Expiration timer next to the Streaks counter" }, + "sort_social_tab_by_streak_length": { + "name": "Sort Social Tab by Streak Length", + "description": "Shows friends with streaks first, ordered from the longest streak to the shortest in the social tab and friend picker" + }, "hide_friend_feed_entry": { "name": "Hide Friend Feed Entry", "description": "Hides a specific friend from the Friend Feed\nUse the social tab to manage this feature" @@ -1253,6 +1278,16 @@ "description": "The custom Snap Score you want to display (max 9,999,999)" } } + }, + "spoof_followers_count": { + "name": "Spoof Followers Count", + "description": "Spoof your follower count on your profile (local only).", + "properties": { + "custom_followers_count": { + "name": "Custom Followers Count", + "description": "Number to show (digits only)." + } + } } } }, @@ -1510,7 +1545,10 @@ "name": "Bypass Message Action Restrictions", "description": "Allows you to react to a snap without having opened it or to save an unsaveable message" }, - "pre_fetch_snaps": { "name": "Snap Pre-Fetch", "description": "Fetches snap media into Snapchat's internal cache once the snap arrives. Note: Turning it on increases background data usage." }, + "pre_fetch_snaps": { + "name": "Snap Pre-Fetch", + "description": "Fetches snap media into Snapchat's internal cache once the snap arrives. Note: Turning it on increases background data usage." + }, "remove_groups_locked_status": { "name": "Remove Groups Locked Status", "description": "Allows you to view group information after being kicked" @@ -1731,9 +1769,12 @@ }, "thermal_protection": { "name": "Thermal Protection", - "description": "Automatically throttles the engine and increases delays if the device temperature exceeds 40°C to prevent overheating" + "description": "Automatically throttles the engine and increases delays if the device temperature exceeds 40\u00b0C to prevent overheating" + }, + "only_on_wifi": { + "name": "Auto Open only on Wi-Fi", + "description": "Only process queue when connected to a Wi-Fi network to save mobile data" }, - "only_on_wifi": { "name": "Auto Open only on Wi-Fi", "description": "Only process queue when connected to a Wi-Fi network to save mobile data" }, "content_type_snap": "Snap", "only_when_idle": { "name": "Auto Open Schedule", @@ -1743,41 +1784,9 @@ "name": "Auto Open Scheduler", "description": "Define the start and end times for scheduled throttled processing." }, - "safe_processing": { "name": "Auto Open with stealth pace", "description": "Adds variable delays and natural breaks to remain undetected. Turn off for maximum speed." } - } - }, - "pre_fetch_snaps": { "name": "Snap Pre-Fetch", "description": "Fetches snap media into Snapchat's internal cache once the snap arrives. Note: Turning it on increases background data usage." }, - "instant_translation": { - "name": "Message Translator", - "description": "Configure the message translator" - }, - "auto_delete_sent_messages": { - "name": "Auto Delete Sent Messages", - "description": "Automatically deletes sent messages after a specified time period", - "properties": { - "allow_running_in_background": { - "name": "Allow Running in Background", - "description": "Allows Auto Delete Sent Messages to run in the background. Note: This will significantly drain your battery" - }, - "delete_after_value": { - "name": "Delete After (value)", - "description": "Time value before deleting the sent message" - }, - "delete_after_unit": { - "name": "Time Unit", - "description": "Select the time unit for deletion delay" - }, - "message_types": { - "name": "Message Types", - "description": "Select which message types should be auto-deleted" - }, - "show_countdown": { - "name": "Show Countdown", - "description": "Show countdown before deleting the message" - }, - "show_notification": { - "name": "Show Notification", - "description": "Show notification during countdown" + "safe_processing": { + "name": "Auto Open with stealth pace", + "description": "Adds variable delays and natural breaks to remain undetected. Turn off for maximum speed." } } }, @@ -1835,6 +1844,36 @@ } } }, + "auto_delete_sent_messages": { + "name": "Auto Delete Sent Messages", + "description": "Automatically deletes sent messages after a specified time period", + "properties": { + "allow_running_in_background": { + "name": "Allow Running in Background", + "description": "Allows Auto Delete Sent Messages to run in the background. Note: This will significantly drain your battery" + }, + "delete_after_value": { + "name": "Delete After (value)", + "description": "Time value before deleting the sent message" + }, + "delete_after_unit": { + "name": "Time Unit", + "description": "Select the time unit for deletion delay" + }, + "message_types": { + "name": "Message Types", + "description": "Select which message types should be auto-deleted" + }, + "show_countdown": { + "name": "Show Countdown", + "description": "Show countdown before deleting the message" + }, + "show_notification": { + "name": "Show Notification", + "description": "Show notification during countdown" + } + } + }, "scheduled_send_allow_running_in_background": { "name": "Allow Scheduled Send to Run in Background", "description": "Keep scheduled messages processing while Snapchat is in the background" @@ -2120,7 +2159,15 @@ "name": "HEVC Recording", "description": "Uses HEVC (H.265) codec for video recording" }, - "camera_tweaks": { "name": "Upgraded Camera Engine", "description": "Enables professional hardware ISP processing modes for better dynamic range" }, "audio_video": { "name": "Upgraded Audio and Video", "description": "Increases Video bitrate to 30Mbps and Audio to 320kbps/48kHz" }, "video_record_timer": { + "camera_tweaks": { + "name": "Upgraded Camera Engine", + "description": "Enables professional hardware ISP processing modes for better dynamic range" + }, + "audio_video": { + "name": "Upgraded Audio and Video", + "description": "Increases Video bitrate to 30Mbps and Audio to 320kbps/48kHz" + }, + "video_record_timer": { "name": "Video Recording Timer", "description": "Shows a recording timer overlay when recording video" }, @@ -2690,7 +2737,11 @@ } } }, - "network_optimization": { "name": "Improved Network Connectivity", "description": "Optimizes network socket buffers for maximum stability and high-speed upload/download performance" }, "better_transcript": { + "network_optimization": { + "name": "Improved Network Connectivity", + "description": "Optimizes network socket buffers for maximum stability and high-speed upload/download performance" + }, + "better_transcript": { "name": "Better Transcript", "description": "Improves the voice note transcript", "properties": { @@ -2890,6 +2941,8 @@ "unsaveable_messages": "\u2b07\ufe0f Unsaveable Messages", "auto_open_snaps": "\ud83d\udcf7 Auto Open Snaps", "stealth": "\ud83d\udc7b Full Stealth Mode", + "snap_stealth": "\ud83d\udcf7 Snap Stealth Mode", + "chat_stealth": "\ud83d\udcac Chat Stealth Mode", "auto_reply": "\ud83d\udce8 Auto Reply", "auto_delete_sent_messages": "\ud83d\uddd1\ufe0f Auto Delete Sent Messages", "mark_chat_as_read": "\ud83d\udcd6 Mark Chat as Read", @@ -3251,7 +3304,10 @@ "platform_indicator": "Adds the platform icon from which a media was sent (e.g. Android, iOS, Web)", "location_indicator": "Adds a \ud83d\udccd icon to snaps when they have been sent with location enabled", "ovf_editor_indicator": "Indicates if a snap has been sent using OVF Editor", - "director_mode_indicator": "Adds a \u270f\ufe0f icon to snaps when they have been sent using Director Mode, which can be used to send gallery images as snaps" + "director_mode_indicator": "Adds a \u270f\ufe0f icon to snaps when they have been sent using Director Mode, which can be used to send gallery images as snaps", + "memories_indicator": "Adds a \ud83d\udcd6 icon to snaps that were re-sent from Memories instead of being captured with the live camera", + "skip_own_indicators": "Hides indicator icons on your own sent snaps (Self-Snaps) \ud83d\udc64", + "disable_indicators_in_groups": "Disables all indicator icons in group conversations to reduce UI clutter \ud83d\udc65" }, "auto_mark_as_read": { "conversation_read": "Mark conversation as read when sending a message", @@ -3832,7 +3888,12 @@ "export_failed_toast": "Failed to export account. Check logs for more info.", "forced_logout_toast": "Removed account due to forced logout" }, - "auto_open_snaps": { "title": "Auto Open Snaps", "processed_count": "Opened", "queue_size": "Queue", "action_reset": "Reset Statistics", "priority_title": "Auto Open Snaps (Priority)", + "auto_open_snaps": { + "title": "Auto Open Snaps", + "processed_count": "Opened", + "queue_size": "Queue", + "action_reset": "Reset Count", + "priority_title": "Auto Open Snaps (Priority)", "auto_open_schedule": { "title": "Auto Open Scheduler", "start": "Start", @@ -3849,7 +3910,6 @@ "action_pause": "Pause", "action_resume": "Resume", "action_clear": "Clear Queue", - "action_reset": "Reset Count", "error_content": "Failed to open snap from {sender}: {error}", "resumed_feedback": "Auto Open Resumed", "paused_feedback": "Auto Open Paused", @@ -3866,9 +3926,9 @@ "speed_throttled": "Throttled", "estimated_time": "Estimated Time", "notification_statistics": "STATISTICS", - "notification_total_opened": "Lifetime Opened", + "notification_total_opened": "Total Snaps Opened", "notification_queue_preview": "QUEUE PREVIEW", - "notification_no_snaps_queue": "Monitoring snaps in background...", + "notification_no_snaps_queue": "No snaps in queue.", "queue_cleared": "Queue cleared and statistics reset", "queue_cleared_title": "Queue cleared", "queue_cleared_reset": "Queue Cleared & Reset", @@ -3883,12 +3943,8 @@ "conversation_type_group_chat": "Group Chat", "conversation_type_chat": "Chat", "notification_status": "Status", - "notification_statistics": "STATISTICS", "notification_queue_size": "Queue Size", - "notification_total_opened": "Total Snaps Opened", - "notification_queue_preview": "QUEUE PREVIEW", "notification_processing_continue": "Processing will continue automatically...", - "notification_no_snaps_queue": "No snaps in queue.", "notification_queue_cleared_opened": "Queue cleared ({opened} opened)", "content_type_photo_video_snap": "Photo/Video Snap", "conversation_type_group_with_name": "Group: {name}", @@ -4016,7 +4072,7 @@ "username": "Username", "user_id": "User ID", "posted_on": "Posted", - "loading_username": "Loading…", + "loading_username": "Loading\u2026", "username_copied": "Username copied", "user_id_copied": "User ID copied", "friend_status": "Friend status", @@ -4101,10 +4157,10 @@ "search": { "placeholder": "Search" }, - "filters": { - "newest_first": "Newest first", - "pick_a_date": "Pick a date", - "title": "Filters", + "filters": { + "newest_first": "Newest first", + "pick_a_date": "Pick a date", + "title": "Filters", "search_by": "Search by", "since": "Since", "until": "Until", @@ -4311,8 +4367,6 @@ "added": "Added", "no_friends_found": "No friends found", "no_messages": "No messages", - "message": "Message", - "type_message": "Type message...", "exporting_memories": "Exporting memories... ({failed} failed)" }, "clear_friend_feed": "Clear Friend Feed", @@ -4457,4 +4511,4 @@ "tasks_remove_all_tasks_title": "Are you sure you want to remove all tasks?", "tasks_remove_selected_tasks_confirm": "Remove {count} selected tasks?", "tasks_remove_all_tasks_confirm": "This will stop all running tasks and clear the history." -} +} \ No newline at end of file diff --git a/common/src/main/assets/lang/hi_IN.json b/common/src/main/assets/lang/hi_IN.json index ac72530d..4e9f3886 100644 --- a/common/src/main/assets/lang/hi_IN.json +++ b/common/src/main/assets/lang/hi_IN.json @@ -2544,7 +2544,10 @@ "platform_indicator": "वह प्लेटफ़ॉर्म आइकन जोड़ता है जहाँ से मीडिया भेजा गया था (उदा. Android, iOS, Web)", "location_indicator": "Snaps में \ud83d\udccd आइकन जोड़ता है जब उन्हें लोकेशन सक्षम के साथ भेजा गया हो", "ovf_editor_indicator": "इंगित करता है कि क्या कोई Snap OVF एडिटर का उपयोग करके भेजा गया है", - "director_mode_indicator": "Snaps में \u270f\ufe0f आइकन जोड़ता है जब उन्हें डायरेक्टर मोड का उपयोग करके भेजा गया हो, जिसका उपयोग गैलरी छवियों को Snaps के रूप में भेजने के लिए किया जा सकता है" + "director_mode_indicator": "Snaps में \u270f\ufe0f आइकन जोड़ता है जब उन्हें डायरेक्टर मोड का उपयोग करके भेजा गया हो, जिसका उपयोग गैलरी छवियों को Snaps के रूप में भेजने के लिए किया जा सकता है", + "memories_indicator": "मेमोरीज़ से फिर से भेजे गए Snaps में \ud83d\udcd6 आइकन जोड़ता है", + "skip_own_indicators": "अपने स्वयं के भेजे गए Snaps पर संकेतक आइकन छुपाता है \ud83d\udc64", + "disable_indicators_in_groups": "UI अव्यवस्था को कम करने के लिए समूह वार्तालापों में सभी संकेतकों को अक्षम करता है \ud83d\udc65" }, "auto_mark_as_read": { "conversation_read": "संदेश भेजते समय वार्तालाप को पढ़े गए के रूप में चिह्नित करें", diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/bridge/wrapper/LoggerWrapper.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/bridge/wrapper/LoggerWrapper.kt index d72f3dac..330b5ca5 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/bridge/wrapper/LoggerWrapper.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/bridge/wrapper/LoggerWrapper.kt @@ -2,6 +2,7 @@ package me.eternal.purrfectsnap.common.bridge.wrapper import android.content.ContentValues import android.content.Context +import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.net.Uri import com.google.gson.GsonBuilder @@ -20,6 +21,7 @@ import me.eternal.purrfectsnap.common.util.ktx.getLongOrNull import me.eternal.purrfectsnap.common.util.ktx.getStringOrNull import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader import java.io.File +import java.io.InputStream import java.util.UUID class LoggedMessage( @@ -70,10 +72,74 @@ data class TrackerLog( } } +data class LoggerConversationExportTarget( + val conversationId: String, + val groupTitle: String?, + val usernames: List, + val userIds: List, + val messageCount: Int +) + +data class ConversationExportResult( + val messageCount: Int, + val chatEditCount: Int, + val trackerEventCount: Int +) + +data class DatabaseImportResult( + val messageCount: Int, + val storyCount: Int +) + class LoggerWrapper( val databaseFile: File, private val readOnly: Boolean = false ): LoggerInterface.Stub() { + companion object { + private val MESSAGE_LOGGER_SCHEMA = mapOf( + "messages" to listOf( + "id INTEGER PRIMARY KEY", + "message_id BIGINT", + "conversation_id VARCHAR", + "user_id CHAR(36)", + "username VARCHAR", + "send_timestamp BIGINT", + "added_timestamp BIGINT", + "group_title VARCHAR", + "message_data BLOB" + ), + "chat_edits" to listOf( + "id INTEGER PRIMARY KEY", + "edit_number INTEGER", + "added_timestamp BIGINT", + "conversation_id VARCHAR", + "message_id BIGINT", + "message_text BLOB" + ), + "stories" to listOf( + "id INTEGER PRIMARY KEY", + "added_timestamp BIGINT", + "user_id VARCHAR", + "posted_timestamp BIGINT", + "created_timestamp BIGINT", + "url VARCHAR", + "encryption_key BLOB", + "encryption_iv BLOB" + ), + "tracker_events" to listOf( + "id INTEGER PRIMARY KEY", + "timestamp BIGINT", + "conversation_id CHAR(36)", + "conversation_title VARCHAR", + "is_group BOOLEAN", + "username VARCHAR", + "user_id VARCHAR", + "event_type VARCHAR", + "data VARCHAR" + ) + ) + } + constructor(context: Context, uri: Uri? = null): this( uri?.path?.let { File(it) } ?: File(context.getDatabasePath(InternalFileHandleType.MESSAGE_LOGGER.fileName).absolutePath), uri != null @@ -84,68 +150,190 @@ class LoggerWrapper( private val coroutineScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1)) private val gson by lazy { GsonBuilder().create() } + private fun openDatabase(file: File, readOnly: Boolean): SQLiteDatabase { + val dbFlags = if (readOnly) { + SQLiteDatabase.OPEN_READONLY + } else { + SQLiteDatabase.CREATE_IF_NECESSARY or SQLiteDatabase.OPEN_READWRITE + } + return SQLiteDatabase.openDatabase(file.absolutePath, null, dbFlags).also { openedDatabase -> + if (!readOnly) { + SQLiteDatabaseHelper.createTablesFromSchema(openedDatabase, MESSAGE_LOGGER_SCHEMA) + } + } + } + + private fun closeDatabaseLocked() { + _database?.takeIf { it.isOpen }?.close() + _database = null + } + private val database get() = synchronized(this) { _database?.takeIf { it.isOpen } ?: run { - _database?.close() - val dbFlags = if (readOnly) SQLiteDatabase.OPEN_READONLY else SQLiteDatabase.CREATE_IF_NECESSARY or SQLiteDatabase.OPEN_READWRITE - val openedDatabase = SQLiteDatabase.openDatabase(databaseFile.absolutePath, null, dbFlags) - if (!readOnly) { - SQLiteDatabaseHelper.createTablesFromSchema(openedDatabase, mapOf( - "messages" to listOf( - "id INTEGER PRIMARY KEY", - "message_id BIGINT", - "conversation_id VARCHAR", - "user_id CHAR(36)", - "username VARCHAR", - "send_timestamp BIGINT", - "added_timestamp BIGINT", - "group_title VARCHAR", - "message_data BLOB" - ), - "chat_edits" to listOf( - "id INTEGER PRIMARY KEY", - "edit_number INTEGER", - "added_timestamp BIGINT", - "conversation_id VARCHAR", - "message_id BIGINT", - "message_text BLOB" - ), - "stories" to listOf( - "id INTEGER PRIMARY KEY", - "added_timestamp BIGINT", - "user_id VARCHAR", - "posted_timestamp BIGINT", - "created_timestamp BIGINT", - "url VARCHAR", - "encryption_key BLOB", - "encryption_iv BLOB" - ), - "tracker_events" to listOf( - "id INTEGER PRIMARY KEY", - "timestamp BIGINT", - "conversation_id CHAR(36)", - "conversation_title VARCHAR", - "is_group BOOLEAN", - "username VARCHAR", - "user_id VARCHAR", - "event_type VARCHAR", - "data VARCHAR" - ) - )) - } + closeDatabaseLocked() + val openedDatabase = openDatabase(databaseFile, readOnly) _database = openedDatabase openedDatabase } } protected fun finalize() { - _database?.close() + synchronized(this) { + closeDatabaseLocked() + } } fun init() { } + private fun resolveDatabaseSidecars(file: File): List { + return listOf( + file, + File("${file.absolutePath}-wal"), + File("${file.absolutePath}-shm"), + File("${file.absolutePath}-journal") + ) + } + + private fun deleteIfExists(file: File) { + if (file.exists() && !file.delete()) { + throw IllegalStateException("Failed to delete ${file.name}") + } + } + + private fun replaceFile(source: File, target: File) { + if (!source.exists()) { + throw IllegalStateException("Missing source file ${source.name}") + } + if (source.renameTo(target)) return + source.inputStream().use { input -> + target.outputStream().use { output -> + input.copyTo(output) + } + } + if (!source.delete()) { + throw IllegalStateException("Failed to delete temporary file ${source.name}") + } + } + + private fun requireCompatibleSchema(db: SQLiteDatabase) { + MESSAGE_LOGGER_SCHEMA.forEach { (tableName, columns) -> + val existingColumns = mutableListOf() + db.rawQuery("PRAGMA table_info($tableName)", null).use { cursor -> + while (cursor.moveToNext()) { + val columnName = cursor.getStringOrNull("name") ?: continue + val columnType = cursor.getStringOrNull("type") ?: "" + existingColumns.add("$columnName $columnType".trim()) + } + } + if (existingColumns.isEmpty()) { + throw IllegalStateException("Selected database is missing required table $tableName") + } + + val missingColumns = columns.filter { expectedColumn -> + !expectedColumn.uppercase().startsWith("PRIMARY KEY") && + existingColumns.none { existingColumn -> expectedColumn.startsWith(existingColumn) } + } + if (missingColumns.isNotEmpty()) { + throw IllegalStateException( + "Selected database has incompatible schema for $tableName: ${missingColumns.joinToString()}" + ) + } + } + } + + private fun requireIntegrity(db: SQLiteDatabase) { + val integrityResult = db.rawQuery("PRAGMA integrity_check(1)", null).use { cursor -> + if (!cursor.moveToFirst()) null else cursor.getString(0) + } + if (integrityResult == null || !integrityResult.equals("ok", ignoreCase = true)) { + throw IllegalStateException("Selected database failed integrity check: ${integrityResult ?: "unknown"}") + } + } + + private fun getTableCount(db: SQLiteDatabase, tableName: String): Int { + return db.rawQuery("SELECT COUNT(*) FROM $tableName", null).use { cursor -> + if (!cursor.moveToFirst()) 0 else cursor.getInt(0) + } + } + + fun importDatabase(inputStream: InputStream): DatabaseImportResult { + if (readOnly) { + throw IllegalStateException("Cannot import into read-only logger") + } + + val databaseDir = databaseFile.parentFile + ?: throw IllegalStateException("Cannot resolve message logger database directory") + if (!databaseDir.exists() && !databaseDir.mkdirs()) { + throw IllegalStateException("Failed to create message logger directory") + } + + val tempFile = File( + databaseDir, + "${databaseFile.name}.import-${System.currentTimeMillis()}-${UUID.randomUUID()}" + ) + val backupFile = File( + databaseDir, + "${databaseFile.name}.backup-${System.currentTimeMillis()}-${UUID.randomUUID()}" + ) + + try { + inputStream.use { input -> + tempFile.outputStream().use { output -> + val copiedBytes = input.copyTo(output) + if (copiedBytes <= 0L) { + throw IllegalStateException("Selected backup is empty") + } + } + } + + openDatabase(tempFile, readOnly = true).use { importedDatabase -> + requireIntegrity(importedDatabase) + requireCompatibleSchema(importedDatabase) + } + + synchronized(this) { + closeDatabaseLocked() + val hadExistingDatabase = databaseFile.exists() + if (hadExistingDatabase) { + databaseFile.inputStream().use { input -> + backupFile.outputStream().use { output -> + input.copyTo(output) + } + } + } + + try { + resolveDatabaseSidecars(databaseFile).forEach(::deleteIfExists) + replaceFile(tempFile, databaseFile) + resolveDatabaseSidecars(databaseFile).filter { it != databaseFile }.forEach(::deleteIfExists) + + val reopenedDatabase = openDatabase(databaseFile, readOnly = false) + val importResult = DatabaseImportResult( + messageCount = getTableCount(reopenedDatabase, "messages"), + storyCount = getTableCount(reopenedDatabase, "stories") + ) + _database = reopenedDatabase + deleteIfExists(backupFile) + return importResult + } catch (throwable: Throwable) { + runCatching { + resolveDatabaseSidecars(databaseFile).forEach(::deleteIfExists) + if (backupFile.exists()) { + replaceFile(backupFile, databaseFile) + } + } + closeDatabaseLocked() + throw throwable + } + } + } finally { + runCatching { deleteIfExists(tempFile) } + runCatching { deleteIfExists(backupFile) } + } + } + override fun getLoggedIds(conversationId: Array, limit: Int): LongArray { if (conversationId.any { runCatching { UUID.fromString(it) }.isFailure @@ -425,6 +613,223 @@ class LoggerWrapper( return ConversationInfo(conversationId, usernames.size, groupTitle, usernames) } + fun getConversationExportTargets(): List { + val groupedConversations = mutableListOf>() + database.rawQuery( + "SELECT conversation_id, MAX(group_title) AS group_title, COUNT(*) AS message_count, MAX(send_timestamp) AS last_timestamp " + + "FROM messages WHERE conversation_id IS NOT NULL AND TRIM(conversation_id) != '' " + + "GROUP BY conversation_id ORDER BY last_timestamp DESC", + null + ).use { cursor -> + while (cursor.moveToNext()) { + val conversationId = cursor.getStringOrNull("conversation_id")?.takeIf { it.isNotBlank() } ?: continue + groupedConversations.add( + Triple( + conversationId, + cursor.getStringOrNull("group_title"), + cursor.getIntOrNull("message_count") ?: 0 + ) + ) + } + } + + return groupedConversations.map { (conversationId, groupTitle, messageCount) -> + val userIds = linkedSetOf() + val usernames = linkedSetOf() + database.rawQuery( + "SELECT DISTINCT user_id, username FROM messages WHERE conversation_id = ?", + arrayOf(conversationId) + ).use { cursor -> + while (cursor.moveToNext()) { + cursor.getStringOrNull("user_id")?.takeIf { it.isNotBlank() }?.let { userIds.add(it) } + cursor.getStringOrNull("username")?.takeIf { it.isNotBlank() }?.let { usernames.add(it) } + } + } + LoggerConversationExportTarget( + conversationId = conversationId, + groupTitle = groupTitle, + usernames = usernames.toList(), + userIds = userIds.toList(), + messageCount = messageCount + ) + } + } + + fun exportConversationDatabase( + outputFile: File, + conversationId: String, + userIds: Collection = emptyList() + ): ConversationExportResult { + val normalizedConversationId = conversationId.trim().takeIf { it.isNotEmpty() } + ?: throw IllegalArgumentException("Conversation ID cannot be empty") + val normalizedUserIds = userIds + .mapNotNull { it.trim().takeIf(String::isNotEmpty) } + .toSet() + .toMutableSet() + .also { ids -> + if (ids.isEmpty()) { + database.rawQuery( + "SELECT DISTINCT user_id FROM messages WHERE conversation_id = ? AND user_id IS NOT NULL AND TRIM(user_id) != ''", + arrayOf(normalizedConversationId) + ).use { cursor -> + while (cursor.moveToNext()) { + cursor.getStringOrNull("user_id")?.takeIf { it.isNotBlank() }?.let { ids.add(it) } + } + } + } + } + + outputFile.parentFile?.mkdirs() + if (outputFile.exists() && !outputFile.delete()) { + throw IllegalStateException("Failed to prepare export file") + } + + val outputDatabase = SQLiteDatabase.openDatabase( + outputFile.absolutePath, + null, + SQLiteDatabase.CREATE_IF_NECESSARY or SQLiteDatabase.OPEN_READWRITE + ) + var transactionStarted = false + try { + SQLiteDatabaseHelper.createTablesFromSchema(outputDatabase, MESSAGE_LOGGER_SCHEMA) + outputDatabase.beginTransaction() + transactionStarted = true + + val messageWhereClause = buildString { + append("conversation_id = ?") + if (normalizedUserIds.isNotEmpty()) { + append(" AND user_id IN (${normalizedUserIds.joinToString(",") { "?" }})") + } + } + val messageWhereArgs = mutableListOf(normalizedConversationId).apply { + addAll(normalizedUserIds) + }.toTypedArray() + + val messageCount = copyQueryRows( + sourceQuery = "SELECT * FROM messages WHERE $messageWhereClause ORDER BY send_timestamp ASC", + sourceArgs = messageWhereArgs, + targetDatabase = outputDatabase, + targetTable = "messages" + ) + + val chatEditCount = copyQueryRows( + sourceQuery = "SELECT * FROM chat_edits WHERE conversation_id = ? AND message_id IN (SELECT message_id FROM messages WHERE $messageWhereClause) ORDER BY added_timestamp ASC", + sourceArgs = arrayOf(normalizedConversationId, *messageWhereArgs), + targetDatabase = outputDatabase, + targetTable = "chat_edits" + ) + + val trackerWhereClause = buildString { + append("conversation_id = ?") + if (normalizedUserIds.isNotEmpty()) { + append(" AND user_id IN (${normalizedUserIds.joinToString(",") { "?" }})") + } + } + val trackerArgs = mutableListOf(normalizedConversationId).apply { + addAll(normalizedUserIds) + }.toTypedArray() + val trackerEventCount = copyQueryRows( + sourceQuery = "SELECT * FROM tracker_events WHERE $trackerWhereClause ORDER BY timestamp ASC", + sourceArgs = trackerArgs, + targetDatabase = outputDatabase, + targetTable = "tracker_events" + ) + + outputDatabase.setTransactionSuccessful() + return ConversationExportResult( + messageCount = messageCount, + chatEditCount = chatEditCount, + trackerEventCount = trackerEventCount + ) + } finally { + if (transactionStarted) { + outputDatabase.endTransaction() + } + outputDatabase.close() + } + } + + private fun cursorToContentValues(cursor: Cursor): ContentValues { + return ContentValues(cursor.columnCount).apply { + for (columnIndex in 0 until cursor.columnCount) { + val columnName = cursor.getColumnName(columnIndex) + when (cursor.getType(columnIndex)) { + Cursor.FIELD_TYPE_NULL -> putNull(columnName) + Cursor.FIELD_TYPE_INTEGER -> put(columnName, cursor.getLong(columnIndex)) + Cursor.FIELD_TYPE_FLOAT -> put(columnName, cursor.getDouble(columnIndex)) + Cursor.FIELD_TYPE_STRING -> put(columnName, cursor.getString(columnIndex)) + Cursor.FIELD_TYPE_BLOB -> put(columnName, cursor.getBlob(columnIndex)) + } + } + } + } + + private fun copyQueryRows( + sourceQuery: String, + sourceArgs: Array? = null, + targetDatabase: SQLiteDatabase, + targetTable: String + ): Int { + var rowCount = 0 + database.rawQuery(sourceQuery, sourceArgs).use { cursor -> + while (cursor.moveToNext()) { + targetDatabase.insert(targetTable, null, cursorToContentValues(cursor)) + rowCount++ + } + } + return rowCount + } + + private fun cursorToLoggedMessage(cursor: Cursor): LoggedMessage? { + return LoggedMessage( + messageId = cursor.getLongOrNull("message_id") ?: return null, + conversationId = cursor.getStringOrNull("conversation_id") ?: return null, + userId = cursor.getStringOrNull("user_id") ?: return null, + username = cursor.getStringOrNull("username") ?: return null, + sendTimestamp = cursor.getLongOrNull("send_timestamp") ?: return null, + addedTimestamp = cursor.getLongOrNull("added_timestamp") ?: return null, + groupTitle = cursor.getStringOrNull("group_title"), + messageData = cursor.getBlobOrNull("message_data") ?: return null + ) + } + + fun forEachConversationMessage( + conversationId: String, + userIds: Collection = emptyList(), + orderAscending: Boolean = true, + block: (LoggedMessage) -> Unit + ): Int { + val normalizedConversationId = conversationId.trim().takeIf { it.isNotEmpty() } + ?: throw IllegalArgumentException("Conversation ID cannot be empty") + val normalizedUserIds = userIds + .mapNotNull { it.trim().takeIf(String::isNotEmpty) } + .toSet() + + val whereClause = buildString { + append("conversation_id = ?") + if (normalizedUserIds.isNotEmpty()) { + append(" AND user_id IN (${normalizedUserIds.joinToString(",") { "?" }})") + } + } + val whereArgs = mutableListOf(normalizedConversationId).apply { + addAll(normalizedUserIds) + }.toTypedArray() + + var total = 0 + database.rawQuery( + "SELECT * FROM messages WHERE $whereClause ORDER BY send_timestamp ${if (orderAscending) "ASC" else "DESC"}", + whereArgs + ).use { cursor -> + while (cursor.moveToNext()) { + cursorToLoggedMessage(cursor)?.let { loggedMessage -> + block(loggedMessage) + total++ + } + } + } + return total + } + override fun getChatEdits(conversationId: String, messageId: Long): List { val edits = mutableListOf() database.rawQuery( @@ -483,16 +888,7 @@ class LoggerWrapper( arrayOf(conversationId, fromTimestamp.toString()) ).use { while (it.moveToNext() && messages.size < limit) { - val message = LoggedMessage( - messageId = it.getLongOrNull("message_id") ?: continue, - conversationId = it.getStringOrNull("conversation_id") ?: continue, - userId = it.getStringOrNull("user_id") ?: continue, - username = it.getStringOrNull("username") ?: continue, - sendTimestamp = it.getLongOrNull("send_timestamp") ?: continue, - addedTimestamp = it.getLongOrNull("added_timestamp") ?: continue, - groupTitle = it.getStringOrNull("group_title"), - messageData = it.getBlobOrNull("message_data") ?: continue - ) + val message = cursorToLoggedMessage(it) ?: continue if (filter != null && !filter(message)) continue messages.add(message) } diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/ConfigContainer.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/ConfigContainer.kt index aeb87e87..4848d287 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/ConfigContainer.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/ConfigContainer.kt @@ -1,7 +1,6 @@ package me.eternal.purrfectsnap.common.config import android.content.Context -import com.google.gson.JsonNull import com.google.gson.JsonObject import me.eternal.purrfectsnap.common.logger.AbstractLogger import kotlin.reflect.KProperty @@ -80,9 +79,7 @@ open class ConfigContainer( properties.forEach { (propertyKey, propertyValue) -> if (!exportSensitiveData && propertyKey.params.flags.contains(ConfigFlag.SENSITIVE)) return@forEach if (!includeSavedLocations && propertyKey.dataType.type == DataProcessors.Type.MAP_COORDINATES) return@forEach - val serializedValue = propertyValue.getRaw()?.let { - propertyKey.dataType.serializeAny(it, exportSensitiveData, includeSavedLocations) - } ?: JsonNull.INSTANCE + val serializedValue = propertyValue.getRaw()?.let { propertyKey.dataType.serializeAny(it, exportSensitiveData, includeSavedLocations) } json.add(propertyKey.name, serializedValue) } return json diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/ConfigObjects.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/ConfigObjects.kt index 3071bd38..8fd5732a 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/ConfigObjects.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/ConfigObjects.kt @@ -81,6 +81,7 @@ class ConfigParams( var inputCheck: ((String) -> Boolean)? = { true }, var filenameFilter: ((String) -> Boolean)? = null, var versionCheck: VersionCheck? = null, + var digitsOnlyInput: Boolean = false, ) { val notices get() = _notices?.let { FeatureNotice.entries.filter { flag -> it and flag.id != 0 } } ?: emptyList() val flags get() = _flags?.let { ConfigFlag.entries.filter { flag -> it and flag.id != 0 } } ?: emptyList() diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/Global.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/Global.kt index d2d24343..5b13f14b 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/Global.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/Global.kt @@ -55,9 +55,7 @@ class Global : ConfigContainer() { } } val mediaUploadQualityConfig = container("media_upload_quality", MediaUploadQualityConfig()) - val performanceMode = container("performance_mode", PerformanceModeConfig()) { requireRestart() }.apply { - profile.set("max") - } + val performanceMode = container("performance_mode", PerformanceModeConfig()) { requireRestart() } val disableConfirmationDialogs = multiple("disable_confirmation_dialogs", "erase_message", "remove_friend", "block_friend", "ignore_friend", "hide_friend", "hide_conversation", "clear_conversation") { requireRestart() } val disableMetrics = boolean("disable_metrics") { requireRestart() } val disableStorySections = multiple("disable_story_sections", "friends", "suggested_stories", "following", "discover") { requireRestart(); requireCleanCache() } @@ -80,8 +78,6 @@ class Global : ConfigContainer() { inner class UpdateSettings : ConfigContainer() { val autoUpdateCheck = boolean("auto_update_check", true) - val updateCheckFrequency = unique("update_check_frequency", "daily", "weekly", "monthly") - val updateChannel = unique("update_channel", "stable", "prerelease") } inner class UISettings : ConfigContainer() { diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/Spoof.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/Spoof.kt index f68710b2..8314f0de 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/Spoof.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/Spoof.kt @@ -186,6 +186,9 @@ class Spoof : ConfigContainer(hasGlobalState = true) { val currentProfileSnapshot = string("current_profile_snapshot") { addFlags(ConfigFlag.HIDDEN) } + val profileData = string("profile_data") { + addFlags(ConfigFlag.HIDDEN) + } } inner class SpoofDeviceIdConfig : ConfigContainer() { diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/UserInterfaceTweaks.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/UserInterfaceTweaks.kt index a07ec875..8f5b71df 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/UserInterfaceTweaks.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/UserInterfaceTweaks.kt @@ -33,6 +33,7 @@ class UserInterfaceTweaks : ConfigContainer() { val mapFriendNameTags = boolean("map_friend_nametags") { requireRestart() } val preventMessageListAutoScroll = boolean("prevent_message_list_auto_scroll") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) } val streakExpirationInfo = boolean("streak_expiration_info") { requireRestart() } + val sortSocialTabByStreakLength = boolean("sort_social_tab_by_streak_length").apply { set(true) } val hideFriendFeedEntry = boolean("hide_friend_feed_entry") { requireRestart() } val hideStreakRestore = boolean("hide_streak_restore") { requireRestart() } val hideQuickAddSuggestions = boolean("hide_quick_add_suggestions") { requireRestart() } @@ -56,7 +57,7 @@ class UserInterfaceTweaks : ConfigContainer() { val oldBitmojiSelfie = unique("old_bitmoji_selfie", "2d", "3d") { requireCleanCache() } val disableSpotlight = boolean("disable_spotlight") { requireRestart() } val verticalStoryViewer = boolean("vertical_story_viewer") { requireRestart() } - val messageIndicators = multiple("message_indicators", "encryption_indicator", "platform_indicator", "location_indicator", "ovf_editor_indicator", "director_mode_indicator") { requireRestart() } + val messageIndicators = multiple("message_indicators", "encryption_indicator", "platform_indicator", "location_indicator", "ovf_editor_indicator", "director_mode_indicator", "memories_indicator", "skip_own_indicators", "disable_indicators_in_groups") { requireRestart() } val stealthModeIndicator = boolean("stealth_mode_indicator") { requireRestart() } val editTextOverride = multiple("edit_text_override", "multi_line_chat_input", "bypass_text_input_limit") { requireRestart(); addNotices(FeatureNotice.BAN_RISK, FeatureNotice.INTERNAL_BEHAVIOR) @@ -76,4 +77,14 @@ class UserInterfaceTweaks : ConfigContainer() { } val spoofSnapScore = container("spoof_snap_score", SpoofSnapScore()) { requireRestart() } + + inner class SpoofFollowersCount : ConfigContainer(hasGlobalState = true) { + val customFollowersCount = string("custom_followers_count") { + requireRestart() + digitsOnlyInput = true + inputCheck = { input -> input.isEmpty() || input.all { it.isDigit() } } + } + } + + val spoofFollowersCount = container("spoof_followers_count", SpoofFollowersCount()) { requireRestart() } } diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/data/MessagingCoreObjects.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/data/MessagingCoreObjects.kt index 70c53ec5..b9018d78 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/data/MessagingCoreObjects.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/data/MessagingCoreObjects.kt @@ -49,8 +49,8 @@ enum class MessagingRuleType( val configNotices: Array = emptyArray() ) { STEALTH("stealth", true, Icons.Outlined.TrackChanges), - SNAP_STEALTH("snap_stealth", true, Icons.Outlined.PhotoCamera, showInFriendMenu = false), - CHAT_STEALTH("chat_stealth", true, Icons.Outlined.ChatBubbleOutline, showInFriendMenu = false), + SNAP_STEALTH("snap_stealth", true, Icons.Outlined.PhotoCamera, showInFriendMenu = true), + CHAT_STEALTH("chat_stealth", true, Icons.Outlined.ChatBubbleOutline, showInFriendMenu = true), HIDE_TYPING_INDICATOR("hide_typing_indicator", true, Icons.Outlined.KeyboardHide, defaultValue = "whitelist"), AUTO_DOWNLOAD("auto_download", true, Icons.Outlined.DownloadForOffline), AUTO_SAVE("auto_save", true, Icons.Outlined.Save, defaultValue = "blacklist"), @@ -65,6 +65,7 @@ enum class MessagingRuleType( AUTO_DELETE_SENT_MESSAGES("auto_delete_sent_messages", true, Icons.Outlined.DeleteSweep, defaultValue = "blacklist"); fun translateOptionKey(optionKey: String): String { + if (key.contains("stealth")) return "features.options.friend_feed_menu_buttons.$key" return if (listMode) "rules.properties.$key.options.$optionKey" else "rules.properties.$key.name" } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/bridge/BridgeClient.kt index 276a7510..7ff0c1f4 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/bridge/BridgeClient.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/bridge/BridgeClient.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.content.ServiceConnection import android.os.* import android.util.Log +import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.sync.Semaphore @@ -41,16 +42,111 @@ class BridgeClient( private var continuation: Continuation? = null private val connectSemaphore = Semaphore(permits = 1) private val reconnectSemaphore = Semaphore(permits = 1) - private lateinit var service: BridgeInterface + private val serviceStateLock = Any() + @Volatile + private var service: BridgeInterface? = null + @Volatile + private var serviceBinder: IBinder? = null + @Volatile + private var isBound = false + @Volatile + private var isHandlingServiceConnection = false + private val connectionExecutor = Executors.newSingleThreadExecutor() + private val legacyBindThread = HandlerThread("BridgeClient").apply { start() } + private val legacyBindHandler by lazy { Handler(legacyBindThread.looper) } private val onConnectedCallbacks = mutableListOf Unit>() private var cachePurrfectSnapApkPath: String? = null + private val serviceDeathRecipient = IBinder.DeathRecipient { + clearConnectedService() + } + + private fun clearConnectedServiceLocked() { + serviceBinder?.let { binder -> + runCatching { binder.unlinkToDeath(serviceDeathRecipient, 0) } + } + serviceBinder = null + service = null + } + + private fun clearConnectedService() { + synchronized(serviceStateLock) { + clearConnectedServiceLocked() + } + } + + private fun attachConnectedService(binder: IBinder): Boolean { + synchronized(serviceStateLock) { + clearConnectedServiceLocked() + serviceBinder = binder + service = BridgeInterface.Stub.asInterface(binder) + return runCatching { + binder.linkToDeath(serviceDeathRecipient, 0) + true + }.getOrElse { throwable -> + Log.w("BridgeClient", "Failed to link bridge death recipient", throwable) + clearConnectedServiceLocked() + false + } + } + } + + private fun isServiceAlive(): Boolean { + val binder = serviceBinder ?: service?.asBinder() ?: return false + return binder.isBinderAlive && binder.pingBinder() + } + + private val connectedService: BridgeInterface + get() { + val currentService = service ?: throw DeadObjectException() + val binder = currentService.asBinder() + if (!binder.isBinderAlive || !binder.pingBinder()) throw DeadObjectException() + return currentService + } + + private fun Context.unbindBridgeIfNeeded() { + if (!isBound) return + runCatching { unbindService(this@BridgeClient) }.onFailure { throwable -> + if (throwable !is IllegalArgumentException) throw throwable + } + isBound = false + } + + private fun Context.bindBridge(intent: Intent): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + bindService( + intent, + Context.BIND_AUTO_CREATE, + connectionExecutor, + this@BridgeClient + ) + } else { + this::class.java.methods.firstOrNull { + it.name == "bindServiceAsUser" && it.parameterTypes.size == 5 + }?.invoke( + this, + intent, + this@BridgeClient, + Context.BIND_AUTO_CREATE, + legacyBindHandler, + Process.myUserHandle() + ) as? Boolean ?: false + } + } + + private fun isRecoverableBinderFailure(throwable: Throwable): Boolean { + return throwable is DeadObjectException || + throwable is RemoteException || + throwable.cause is DeadObjectException || + throwable.cause is RemoteException + } + fun addOnConnectedCallback(initNow: Boolean = false, callback: suspend () -> Unit) { synchronized(onConnectedCallbacks) { onConnectedCallbacks.add(callback) } - initNow.takeIf { it && this::service.isInitialized }?.let { + initNow.takeIf { it && isServiceAlive() }?.let { runBlocking { callback() } @@ -67,7 +163,7 @@ class BridgeClient( } suspend fun connect(onFailure: (Throwable) -> Unit): Boolean? { - if (this::service.isInitialized && service.asBinder().pingBinder()) { + if (isServiceAlive()) { return true } @@ -75,10 +171,9 @@ class BridgeClient( val retryDelay = 3000L return withTimeoutOrNull(connectionTimeout) { - var result: Boolean? = null - - for (retry in 0.. (connectionTimeout / retryDelay).toInt()) { - result = withTimeoutOrNull(retryDelay) { + val attempts = (connectionTimeout / retryDelay).toInt() + 1 + repeat(attempts) { attempt -> + val result = withTimeoutOrNull(retryDelay) { suspendCancellableCoroutine { cancellableContinuation -> continuation = cancellableContinuation with(context.androidContext) { @@ -93,31 +188,12 @@ class BridgeClient( runCatching { val intent = Intent() .setClassName(Constants.MODULE_PACKAGE_NAME, "me.eternal.purrfectsnap.bridge.BridgeService") - runCatching { - if (this@BridgeClient::service.isInitialized) { - unbindService(this@BridgeClient) - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - bindService( - intent, - Context.BIND_AUTO_CREATE, - Executors.newSingleThreadExecutor(), - this@BridgeClient - ) - } else { - val handler = Handler(HandlerThread("BridgeClient").apply { start() }.looper) - this::class.java.methods.firstOrNull { - it.name == "bindServiceAsUser" && it.parameterTypes.size == 5 - }?.invoke( - this, - intent, - this@BridgeClient, - Context.BIND_AUTO_CREATE, - handler, - Process.myUserHandle() - ) ?: throw NoSuchMethodException("bindServiceAsUser") + unbindBridgeIfNeeded() + clearConnectedService() + if (!bindBridge(intent)) { + throw IllegalStateException("bindService returned false") } + isBound = true }.onFailure { onFailure(it) resumeContinuation(false) @@ -125,46 +201,82 @@ class BridgeClient( } } } - if (result != null) break + if (result == true) { + return@withTimeoutOrNull true + } + if (attempt + 1 < attempts) { + delay(250L) + } } - result + false } } override fun onServiceConnected(name: ComponentName, service: IBinder) { - this.service = BridgeInterface.Stub.asInterface(service) - runBlocking { - onConnectedCallbacks.forEach { - runCatching { - it() - }.onFailure { - context.log.error("Failed to run onConnectedCallback", it) - } - } - } - cachePurrfectSnapApkPath = this.service.applicationApkPath.also { - if (cachePurrfectSnapApkPath != null && cachePurrfectSnapApkPath != it) { - context.log.verbose("Restarting Snapchat due to PurrfectSnap update") - context.softRestartApp() + isHandlingServiceConnection = true + try { + if (!attachConnectedService(service)) { + resumeContinuation(false) return } + + runBlocking { + onConnectedCallbacks.forEach { + runCatching { + it() + }.onFailure { + context.log.error("Failed to run onConnectedCallback", it) + } + } + } + val remoteApkPath = runCatching { + connectedService.applicationApkPath + }.getOrElse { throwable -> + if (isRecoverableBinderFailure(throwable)) { + Log.w("BridgeClient", "Bridge died during onServiceConnected initialization", throwable) + } else { + Log.e("BridgeClient", "Bridge initialization failed", throwable) + } + clearConnectedService() + resumeContinuation(false) + return + } + cachePurrfectSnapApkPath = remoteApkPath.also { + if (cachePurrfectSnapApkPath != null && cachePurrfectSnapApkPath != it) { + context.log.verbose("Restarting Snapchat due to PurrfectSnap update") + context.softRestartApp() + return + } + } + resumeContinuation(true) + } finally { + isHandlingServiceConnection = false } - resumeContinuation(true) } override fun onNullBinding(name: ComponentName) { + clearConnectedService() + isBound = false resumeContinuation(false) } override fun onServiceDisconnected(name: ComponentName) { + clearConnectedService() + isBound = false continuation = null } + override fun onBindingDied(name: ComponentName) { + clearConnectedService() + isBound = false + resumeContinuation(false) + } + private fun tryReconnect() { runBlocking { reconnectSemaphore.withPermit { - if (service.asBinder().pingBinder()) return@runBlocking + if (isServiceAlive()) return@withPermit Log.d("BridgeClient", "service is dead, restarting") val canLoad = connect { Log.e("BridgeClient", "connection failed", it) @@ -181,13 +293,16 @@ class BridgeClient( return runCatching { block() }.getOrElse { throwable -> - if (throwable is DeadObjectException) { - tryReconnect() - return@getOrElse runCatching { - block() - }.getOrElse { - Log.e("BridgeClient", "service call failed", it) - throw it + if (isRecoverableBinderFailure(throwable)) { + clearConnectedService() + if (!isHandlingServiceConnection) { + tryReconnect() + return@getOrElse runCatching { + block() + }.getOrElse { + Log.e("BridgeClient", "service call failed", it) + throw it + } } } throw throwable @@ -197,15 +312,15 @@ class BridgeClient( fun broadcastLog(tag: String, level: String, message: String) { message.chunked(1024 * 256).forEach { runCatching { - service.broadcastLog(tag, level, it) + connectedService.broadcastLog(tag, level, it) } } } - fun getApplicationApkPath(): String = safeServiceCall { service.applicationApkPath } + fun getApplicationApkPath(): String = safeServiceCall { connectedService.applicationApkPath } fun enqueueDownload(intent: Intent, callback: DownloadCallback) = safeServiceCall { - service.enqueueDownload(intent, callback) + connectedService.enqueueDownload(intent, callback) } fun convertMedia( @@ -215,40 +330,42 @@ class BridgeClient( audioCodec: String?, videoCodec: String? ): ParcelFileDescriptor? = safeServiceCall { - service.convertMedia(input, inputExtension, outputExtension, audioCodec, videoCodec) + connectedService.convertMedia(input, inputExtension, outputExtension, audioCodec, videoCodec) } fun sync(callback: SyncCallback) { if (!context.database.hasMain()) return safeServiceCall { - service.sync(callback) + connectedService.sync(callback) } } fun triggerSync(scope: SocialScope, id: String) = safeServiceCall { - service.triggerSync(scope.key, id) + connectedService.triggerSync(scope.key, id) } fun passGroupsAndFriends(groups: List, friends: List) = safeServiceCall { val serializedGroups = groups.mapNotNull { it.toSerialized() } val serializedFriends = friends.mapNotNull { it.toSerialized() } + + // Binder transaction limit is 1MB. Use 128KB chunks to avoid TransactionTooLargeException. val maxChunkBytes = 128 * 1024 - fun chunkSerialized(values: List): List> { + fun calculateParts(values: List): List> { if (values.isEmpty()) return listOf(emptyList()) val result = mutableListOf>() - val currentChunk = mutableListOf() + var currentChunk = mutableListOf() var currentSize = 0 values.forEach { value -> - val valueSize = value.toByteArray(StandardCharsets.UTF_8).size + 32 + val valueSize = value.toByteArray(Charsets.UTF_8).size + 32 if (currentChunk.isNotEmpty() && currentSize + valueSize > maxChunkBytes) { result += currentChunk.toList() - currentChunk.clear() + currentChunk = mutableListOf() currentSize = 0 } - currentChunk += value + currentChunk.add(value) currentSize += valueSize } @@ -258,73 +375,71 @@ class BridgeClient( return result } - val groupChunks = chunkSerialized(serializedGroups) - val friendChunks = chunkSerialized(serializedFriends) - val chunkCount = maxOf(groupChunks.size, friendChunks.size) + val groupParts = calculateParts(serializedGroups) + val friendParts = calculateParts(serializedFriends) + val totalParts = maxOf(groupParts.size, friendParts.size) - context.log.info( - "Sending social snapshot in $chunkCount chunk(s): " + - "${serializedGroups.size} groups, ${serializedFriends.size} friends" - ) + context.log.info("Synchronizing social data in $totalParts part(s): ${serializedGroups.size} groups, ${serializedFriends.size} friends") - repeat(chunkCount) { index -> - service.passGroupsAndFriends( - groupChunks.getOrElse(index) { emptyList() }, - friendChunks.getOrElse(index) { emptyList() } + repeat(totalParts) { index -> + connectedService.passGroupsAndFriends( + groupParts.getOrElse(index) { emptyList() }, + friendParts.getOrElse(index) { emptyList() }, + index, + totalParts ) } } fun getRules(targetUuid: String): List = safeServiceCall { - service.getRules(targetUuid).mapNotNull { MessagingRuleType.getByName(it) } + connectedService.getRules(targetUuid).mapNotNull { MessagingRuleType.getByName(it) } } fun getRuleIds(ruleType: MessagingRuleType): List = safeServiceCall { - service.getRuleIds(ruleType.key) + connectedService.getRuleIds(ruleType.key) } fun setRule(targetUuid: String, type: MessagingRuleType, state: Boolean) = safeServiceCall { - service.setRule(targetUuid, type.key, state) + connectedService.setRule(targetUuid, type.key, state) } - fun getScopeNotes(id: String): String? = safeServiceCall { service.getScopeNotes(id) } + fun getScopeNotes(id: String): String? = safeServiceCall { connectedService.getScopeNotes(id) } - fun setScopeNotes(id: String, content: String?) = safeServiceCall { service.setScopeNotes(id, content) } + fun setScopeNotes(id: String, content: String?) = safeServiceCall { connectedService.setScopeNotes(id, content) } - fun getAllScopeNotes(): Map = safeServiceCall { service.getAllScopeNotes() } + fun getAllScopeNotes(): Map = safeServiceCall { connectedService.getAllScopeNotes() } - fun setAllScopeNotes(notes: Map) = safeServiceCall { service.setAllScopeNotes(notes) } + fun setAllScopeNotes(notes: Map) = safeServiceCall { connectedService.setAllScopeNotes(notes) } - fun getScriptingInterface(): IScripting? = safeServiceCall { service.scriptingInterface } + fun getScriptingInterface(): IScripting? = safeServiceCall { connectedService.scriptingInterface } - fun getE2eeInterface(): E2eeInterface = safeServiceCall { service.e2eeInterface } + fun getE2eeInterface(): E2eeInterface = safeServiceCall { connectedService.e2eeInterface } - fun getMessageLogger(): LoggerInterface = safeServiceCall { service.logger } + fun getMessageLogger(): LoggerInterface = safeServiceCall { connectedService.logger } - fun getTracker(): TrackerInterface = safeServiceCall { service.tracker } + fun getTracker(): TrackerInterface = safeServiceCall { connectedService.tracker } - fun getAccountStorage(): AccountStorage = safeServiceCall { service.accountStorage } + fun getAccountStorage(): AccountStorage = safeServiceCall { connectedService.accountStorage } - fun getFileHandlerManager(): FileHandleManager = safeServiceCall { service.fileHandleManager } + fun getFileHandlerManager(): FileHandleManager = safeServiceCall { connectedService.fileHandleManager } - fun getLocationManager(): LocationManager = safeServiceCall { service.locationManager } + fun getLocationManager(): LocationManager = safeServiceCall { connectedService.locationManager } - fun getTaskInterface(): TaskInterface = safeServiceCall { service.taskInterface } + fun getTaskInterface(): TaskInterface = safeServiceCall { connectedService.taskInterface } - fun registerMessagingBridge(bridge: MessagingBridge) = safeServiceCall { service.registerMessagingBridge(bridge) } + fun registerMessagingBridge(bridge: MessagingBridge) = safeServiceCall { connectedService.registerMessagingBridge(bridge) } - fun openOverlay(type: OverlayType) = safeServiceCall { service.openOverlay(type.key) } - fun closeOverlay() = safeServiceCall { service.closeOverlay() } + fun openOverlay(type: OverlayType) = safeServiceCall { connectedService.openOverlay(type.key) } + fun closeOverlay() = safeServiceCall { connectedService.closeOverlay() } - fun registerConfigStateListener(listener: ConfigStateListener) = safeServiceCall { service.registerConfigStateListener(listener) } + fun registerConfigStateListener(listener: ConfigStateListener) = safeServiceCall { connectedService.registerConfigStateListener(listener) } - fun getDebugProp(name: String, defaultValue: String? = null): String? = safeServiceCall { service.getDebugProp(name, defaultValue) } + fun getDebugProp(name: String, defaultValue: String? = null): String? = safeServiceCall { connectedService.getDebugProp(name, defaultValue) } fun startCallDownload( startTimestamp: Long, author: String, ): CallDownloadSession { - return safeServiceCall { service.startCallDownload(startTimestamp, author) } + return safeServiceCall { connectedService.startCallDownload(startTimestamp, author) } } } - diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/event/EventDispatcher.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/event/EventDispatcher.kt index 9765cd33..1fd1f29f 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/event/EventDispatcher.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/event/EventDispatcher.kt @@ -47,7 +47,7 @@ class EventDispatcher( cacheHook( methodParam.thisObject()::class.java ) { - hook(bindMethod.get().toString(), HookStage.BEFORE) bindViewMethod@{ param -> + hook(bindMethod.get().toString(), HookStage.AFTER) bindViewMethod@{ param -> val instance = param.thisObject() val view = instance::class.java.methods.firstOrNull { it.name == getViewMethod.get().toString() @@ -161,7 +161,6 @@ class EventDispatcher( adapter = param } ) { - if (canceled) param.setResult(null) postHookEvent() } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/MessagingRuleFeature.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/MessagingRuleFeature.kt index d26192af..cebc3969 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/MessagingRuleFeature.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/MessagingRuleFeature.kt @@ -32,7 +32,7 @@ abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleTyp } && getRuleState() != null } - fun canUseRule(conversationId: String): Boolean { + open fun canUseRule(conversationId: String): Boolean { if (ruleType.key == "translation" && context.config.messaging.instantTranslation.globalState != true) { return false } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ConfigurationOverride.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ConfigurationOverride.kt index a9d66f63..915f21d0 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ConfigurationOverride.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ConfigurationOverride.kt @@ -69,6 +69,16 @@ class ConfigurationOverride : Feature("Configuration Override") { overrideProperty("TRANSCODING_MAX_QUALITY", { context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get() || context.config.messaging.galleryMediaSendOverride.mode.getNullable() != null }, { true }, isAppExperiment = true) + overrideProperty("BYPASS_AD_FEATURE_GATE", { context.config.global.blockAds.get() }, + { true }) + + overrideProperty("SPONSORED_SNAPS_ENABLED", { context.config.global.blockAds.get() }, { false }) + overrideProperty("SPONSORED_SNAP_UPDATE_SPONSORED_FEED_ITEM", { context.config.global.blockAds.get() }, { false }) + + arrayOf("CUSTOM_AD_TRACKER_URL", "CUSTOM_AD_INIT_SERVER_URL", "CUSTOM_AD_SERVER_URL", "INIT_PRIMARY_URL", "INIT_SHADOW_URL", "GRAPHENE_HOST").forEach { + overrideProperty(it, { context.config.global.blockAds.get() }, { "http://127.0.0.1" }) + } + run { val isForceQuality = { _: ConfigKeyInfo -> context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get() || context.config.messaging.galleryMediaSendOverride.mode.getNullable() != null } val level7Value = { _: ConfigKeyInfo -> 700 } @@ -172,15 +182,6 @@ class ConfigurationOverride : Feature("Configuration Override") { }, { false }) - overrideProperty("BYPASS_AD_FEATURE_GATE", { context.config.global.blockAds.get() }, - { true }) - - overrideProperty("SPONSORED_SNAPS_ENABLED", { context.config.global.blockAds.get() }, { false }) - overrideProperty("SPONSORED_SNAP_UPDATE_SPONSORED_FEED_ITEM", { context.config.global.blockAds.get() }, { false }) - - arrayOf("CUSTOM_AD_TRACKER_URL", "CUSTOM_AD_INIT_SERVER_URL", "CUSTOM_AD_SERVER_URL", "INIT_PRIMARY_URL", "INIT_SHADOW_URL", "GRAPHENE_HOST").forEach { - overrideProperty(it, { context.config.global.blockAds.get() }, { "http://127.0.0.1" }) - } overrideProperty("GIFTING_CHAT_BIRTHDAY_UPSELL_ENABLED", { context.config.userInterface.hideUiComponents.get().contains("hide_snapchat_plus_gift_reminders") }, { false }) classReference.getAsClass()?.hook( diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/AutoOpenSnaps.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/AutoOpenSnaps.kt index f413b786..6aebaed9 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/AutoOpenSnaps.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/AutoOpenSnaps.kt @@ -1,6 +1,5 @@ package me.eternal.purrfectsnap.core.features.impl.experiments -import android.app.ActivityManager import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -17,11 +16,11 @@ import androidx.core.content.edit import com.google.gson.Gson import com.google.gson.reflect.TypeToken import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import me.eternal.purrfectsnap.bridge.AutoOpenInterface import me.eternal.purrfectsnap.common.config.PropertyValue +import me.eternal.purrfectsnap.common.config.ModConfig import me.eternal.purrfectsnap.common.data.ContentType import me.eternal.purrfectsnap.common.data.MessageState import me.eternal.purrfectsnap.common.data.MessageUpdate @@ -33,6 +32,7 @@ import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hookConstructor import java.util.* +import java.util.Objects import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger @@ -42,7 +42,7 @@ import kotlin.random.Random /** * AutoOpenSnaps: High-performance engine with real-time diagnostics. - * Optimized for 20+ snaps/s with accurate stats and background resilience. + * Optimized for background resilience and industrial stability. */ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.AUTO_OPEN_SNAPS) { companion object { @@ -54,12 +54,12 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private const val NOTIFICATION_GROUP_KEY = "purrfectsnap.AUTO_OPEN" private const val PREF_TOTAL_OPENED = "auto_open_total_opened" private const val PREF_SESSION_START = "auto_open_session_start" - - private const val LAZY_SAVE_INTERVAL_MS = 600_000L + private const val PREF_SAVED_QUEUE = "auto_open_saved_queue" } private val gson = Gson() private val isPaused = AtomicBoolean(false) + private val isScreenOn = AtomicBoolean(true) private val engineActive = AtomicBoolean(true) private val totalProcessed = AtomicInteger(0) private val sessionProcessed = AtomicInteger(0) @@ -67,9 +67,10 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private val averageProcessingTime = AtomicLong(800) private val lastSnapProcessedAt = AtomicLong(0) - private val snapChannel = Channel(Channel.UNLIMITED) + private val snapQueue = MutableSharedFlow(extraBufferCapacity = 100, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val openedSnapsIds = ConcurrentHashMap.newKeySet() private val queuedSnaps = LinkedList() + private val deadLetterQueue = mutableListOf() private var engineJob: Job? = null private val engineDispatcher = Dispatchers.Default.limitedParallelism(1) @@ -78,19 +79,22 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private val prefs by lazy { this@AutoOpenSnaps.context.androidContext.getSharedPreferences("me.eternal.purrfectsnap_preferences", Context.MODE_PRIVATE) } private val messaging by lazy { this@AutoOpenSnaps.context.feature(Messaging::class) } private var wakeLock: PowerManager.WakeLock? = null + private var wakeLockCooldownJob: Job? = null private var currentStatusText = "Monitoring..." private var currentSpeedText = "Full Speed" private var lastNotificationUpdate = 0L + private var lastNotificationStateHash = 0 private val notificationUpdateDelay = 1000L private val pendingNotificationUpdate = AtomicBoolean(false) private val snapTimestamps = LinkedList() private var lastConversationId: String? = null + private var lastQueueActivity = System.currentTimeMillis() - private val isSaving = AtomicBoolean(false) - private val needsSaving = AtomicBoolean(false) + private val lastSaveTime = AtomicLong(System.currentTimeMillis()) private var isThermalThrottled = false private var lastThermalThrottleAt = 0L + private var actionReceiver: BroadcastReceiver? = null private fun logInfo(msg: String) = this@AutoOpenSnaps.context.log.info("[AutoOpenEngine] $msg") private fun logError(msg: String, e: Throwable? = null) = if (e != null) this@AutoOpenSnaps.context.log.error("[AutoOpenEngine] $msg", e) else this@AutoOpenSnaps.context.log.error("[AutoOpenEngine] $msg") @@ -99,7 +103,6 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A val now = System.currentTimeMillis(); val window = 5000L synchronized(snapTimestamps) { snapTimestamps.removeIf { now - it > window } - // Smoother calculation for high-frequency bursts return if (snapTimestamps.isEmpty()) 0.0 else (snapTimestamps.size.toDouble() / (window / 1000.0)) } } @@ -110,10 +113,11 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A } override fun init() { + if (autoOpenConfig.globalState != true) return + restorePersistence() createNotificationChannels() - // NATIVE HOOKS: Ensuring Snapchat never sees the app as "In Background" if ((autoOpenConfig.allowRunningInBackground as PropertyValue).get()) { runCatching { findClass("com.snapchat.client.duplex.DuplexClient\$CppProxy").apply { @@ -121,6 +125,13 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A val state = param.arg(0).toString() if (state == "INACTIVE" || state == "BACKGROUND") param.setResult(null) } + hookConstructor(HookStage.AFTER) { param -> + methods.firstOrNull { it.name == "appStateChanged" }?.let { method -> + val enumClass = method.parameterTypes[0] + val activeState = enumClass.enumConstants?.firstOrNull { it.toString() == "ACTIVE" || it.toString() == "FOREGROUND" } + if (activeState != null) method.invoke(param.thisObject(), activeState) + } + } } findClass("com.snapchat.client.network_manager.NetworkManager\$CppProxy").apply { hook("onAppForegrounded", HookStage.BEFORE) { param -> param.setResult(null) } @@ -129,6 +140,27 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A } } + // Background Watchdog: Periodically verifies engine health + this@AutoOpenSnaps.context.coroutineScope.launch(Dispatchers.Default) { + while (isActive && engineActive.get()) { + val remainingCount = synchronized(queuedSnaps) { queuedSnaps.size } + if (remainingCount > 0) { + lastQueueActivity = System.currentTimeMillis(); acquireWakeLock() + if (!isPaused.get()) snapQueue.tryEmit(System.currentTimeMillis()) + } else { + if (!isPaused.get() && System.currentTimeMillis() - lastQueueActivity > 300000) { + val revived = synchronized(deadLetterQueue) { if (deadLetterQueue.isNotEmpty()) deadLetterQueue.removeAt(0) else null } + if (revived != null) { synchronized(queuedSnaps) { queuedSnaps.add(revived) }; snapQueue.tryEmit(System.currentTimeMillis()) } + } + if (System.currentTimeMillis() - lastQueueActivity > 300000) { + startWakeLockCooldown() + } + } + updateStatusNotification() + delay(30000) // 30s watchdog cycle + } + } + setupReceivers() startEngineWorker() setupDetector() @@ -136,90 +168,106 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private fun startEngineWorker() { engineJob = this@AutoOpenSnaps.context.coroutineScope.launch(engineDispatcher) { - while (engineActive.get()) { - val item = try { snapChannel.receive() } catch (e: Exception) { break } - - while (isPaused.get() && engineActive.get()) { - currentStatusText = "Paused"; updateStatusNotification(); delay(500) - } - if (!engineActive.get()) break + snapQueue.collect { + while (engineActive.get()) { + val item = synchronized(queuedSnaps) { if (queuedSnaps.isNotEmpty()) queuedSnaps.removeAt(0) else null } ?: break - updateStatusNotification() - if (!validateEnvironmentalConstraints()) { - synchronized(queuedSnaps) { queuedSnaps.remove(item) } - continue - } + while (isPaused.get() && engineActive.get()) { + currentStatusText = "Paused"; updateStatusNotification(); delay(500) + } + if (!engineActive.get()) break - // SPEED OPTIMIZATION: Instant switch (40ms) when stealth is off - val isSafe = (autoOpenConfig.safeProcessing as PropertyValue).get() - if (lastConversationId != null && lastConversationId != item.conversationId) { - delay(if (isSafe) (autoOpenConfig.delayBetweenConversations as PropertyValue).get().toLong() else 40L) - } - lastConversationId = item.conversationId - - processSnapItem(item) - lastSnapProcessedAt.set(System.currentTimeMillis()) - - // HIGH SPEED: 10ms floor for 20+ snaps/s - val baseDelay = if (currentSpeedText == "Throttled") 3000L else (autoOpenConfig.delayBetweenSnaps as PropertyValue).get().toLong() - if (isSafe) { - delay(Random.nextLong(baseDelay, baseDelay + 200)) - } else { - delay(baseDelay.coerceAtMost(10)) - } - - if (synchronized(queuedSnaps) { queuedSnaps.isEmpty() }) { - currentStatusText = "Monitoring..." updateStatusNotification() + if (!validateEnvironmentalConstraints()) { + synchronized(queuedSnaps) { queuedSnaps.add(0, item) } + continue + } + + val isSafe = (autoOpenConfig.safeProcessing as PropertyValue).get() + if (lastConversationId != null && lastConversationId != item.conversationId) { + delay(if (isSafe) (autoOpenConfig.delayBetweenConversations as PropertyValue).get().toLong() else 40L) + } + lastConversationId = item.conversationId + + processSnapItem(item) + lastSnapProcessedAt.set(System.currentTimeMillis()) + + // Process at natural network speed when safety is disabled + val baseDelay = if (currentSpeedText == "Throttled") 3000L else (autoOpenConfig.delayBetweenSnaps as PropertyValue).get().toLong() + if (isSafe) { + delay(Random.nextLong(baseDelay, baseDelay + 200)) + } else { + if (baseDelay > 0) delay(baseDelay) + } + + if (synchronized(queuedSnaps) { queuedSnaps.isEmpty() }) { + currentStatusText = "Monitoring..." + updateStatusNotification() + saveQueueToDisk() // Batch complete save + startWakeLockCooldown() + } } } } } private suspend fun processSnapItem(item: SnapQueueItem) { + // Verify database state on background thread before processing + val dbMessage = withContext(Dispatchers.IO) { this@AutoOpenSnaps.context.database.getConversationMessageFromId(item.messageId) } + if (dbMessage?.isViewedByUser == 1) { + return + } + currentStatusText = "Active"; updateStatusNotification() var success = false val startTime = System.currentTimeMillis() for (i in 0 until (autoOpenConfig.retryAttempts as PropertyValue).get()) { if (isPaused.get() || !engineActive.get() || autoOpenConfig.globalState == false) break - - if (messaging.conversationManager == null) { + + if (messaging.conversationManager == null) { runCatching { this@AutoOpenSnaps.context.messagingBridge.triggerSessionStart() } - delay(1000) + delay(1000) } - - success = withContext(Dispatchers.IO) { performOpen(item) } + + success = performOpen(item) if (success) { - // IMPORTANT: Item only removed after successful processing to ensure Stats sync - synchronized(queuedSnaps) { queuedSnaps.remove(item) } sessionProcessed.incrementAndGet(); totalProcessed.incrementAndGet(); recordSpeedTimestamp() + + // Industrial Interval Check: Only write to disk once every 10 minutes during floods + if (System.currentTimeMillis() - lastSaveTime.get() > 600000) { + saveQueueToDisk() + lastSaveTime.set(System.currentTimeMillis()) + } + val duration = System.currentTimeMillis() - startTime averageProcessingTime.set((averageProcessingTime.get() * 0.7 + duration * 0.3).toLong()) - triggerLazySave(); break + break } delay((autoOpenConfig.retryDelay as PropertyValue).get().toLong()) } if (!success && !isPaused.get() && engineActive.get()) { logError("Engine failed to open Snap: ${item.messageId}") - synchronized(queuedSnaps) { queuedSnaps.remove(item) } + synchronized(openedSnapsIds) { openedSnapsIds.remove(item.messageId) } + synchronized(deadLetterQueue) { if (deadLetterQueue.size < 100) deadLetterQueue.add(item) else { deadLetterQueue.removeAt(0); deadLetterQueue.add(item) } } currentStatusText = "Failed: ${item.senderName}"; updateStatusNotification() - openedSnapsIds.remove(item.messageId) } } private suspend fun performOpen(item: SnapQueueItem): Boolean { val manager = messaging.conversationManager ?: return false - return suspendCancellableCoroutine { cont -> - runCatching { - manager.updateMessage(item.conversationId, item.messageId, MessageUpdate.READ) { result -> - if (result == null || result == "DUPLICATEREQUEST") { cont.resume(true) } - else if (item.serverMessageId != 0L) { - manager.updateMessage(item.conversationId, item.serverMessageId, MessageUpdate.READ) { serverResult -> - cont.resume(serverResult == null || serverResult == "DUPLICATEREQUEST") - } - } else { cont.resume(false) } - } - }.onFailure { logError("Bridge Error", it); cont.resume(false) } + return withContext(Dispatchers.Main) { + suspendCancellableCoroutine { cont -> + runCatching { + manager.updateMessage(item.conversationId, item.messageId, MessageUpdate.READ) { result -> + if (result == null || result == "DUPLICATEREQUEST") { cont.resume(true) } + else if (item.serverMessageId != 0L) { + manager.updateMessage(item.conversationId, item.serverMessageId, MessageUpdate.READ) { serverResult -> + cont.resume(serverResult == null || serverResult == "DUPLICATEREQUEST") + } + } else { cont.resume(false) } + } + }.onFailure { logError("Bridge Error", it); cont.resume(false) } + } } } @@ -230,17 +278,17 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A val isIdle = isDeviceIdle() val onlyIdle = (autoOpenConfig.onlyWhenIdle as PropertyValue).get() val inSleepWindow = if (onlyIdle) isInsideSleepWindow() else false - + val wifiStop = (autoOpenConfig.onlyOnWifi as PropertyValue).get() && !isWifi val idleStop = onlyIdle && !isIdle && !inSleepWindow - + when { wifiStop -> { currentStatusText = "Waiting for WiFi..."; delay(5000) } idleStop -> { currentStatusText = "Waiting for Idle..."; delay(5000) } - else -> { + else -> { val thermalActive = (autoOpenConfig.thermalProtection as PropertyValue).get() && isThermalThrottled currentSpeedText = if (inSleepWindow || thermalActive) "Throttled" else "Full Speed" - return true + return true } } updateStatusNotification() @@ -252,52 +300,76 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A this@AutoOpenSnaps.context.event.subscribe(BuildMessageEvent::class, priority = 103) { event -> if (autoOpenConfig.globalState == false || !engineActive.get()) return@subscribe val message = event.message + + // 1. Basic Filters & Self-Check if (message.messageState != MessageState.COMMITTED || message.senderId?.toString() == this@AutoOpenSnaps.context.database.myUserId) return@subscribe - val conversationId = message.messageDescriptor?.conversationId?.toString() ?: return@subscribe + val clientMessageId = message.messageDescriptor?.messageId ?: return@subscribe + val conversationId = message.messageDescriptor?.conversationId?.toString() ?: return@subscribe val serverMessageId = message.orderKey ?: 0L val contentType = message.messageContent?.contentType if (contentType != ContentType.SNAP && contentType != ContentType.EXTERNAL_MEDIA) return@subscribe if (!canUseRule(conversationId)) return@subscribe + + // 2. Memory Gating: Prevent processing the same session snap multiple times if (openedSnapsIds.contains(clientMessageId)) return@subscribe + + // 3. Database Authority: Immediate check to see if snap is already opened + val dbMessage = this@AutoOpenSnaps.context.database.getConversationMessageFromId(clientMessageId) + if (dbMessage?.isViewedByUser == 1) return@subscribe + + // 4. Temporal Gating: Ignore ancient unread snaps (fixes 'Ghost Storm' during sync) + val now = System.currentTimeMillis() + val messageTime = message.messageMetadata?.createdAt ?: 0L + if (now - messageTime > 28_800_000L) { // 8-hour window + return@subscribe + } + openedSnapsIds.add(clientMessageId) - + val senderId = message.senderId?.toString() ?: "unknown" val item = SnapQueueItem(conversationId, clientMessageId, serverMessageId, senderId, getSenderDisplayName(senderId), getConversationType(conversationId, senderId), getSnapContentType(contentType)) - - synchronized(queuedSnaps) { queuedSnaps.add(item) } - snapChannel.trySend(item) - - acquireWakeLock(); updateStatusNotification(); triggerLazySave() - } - } - private fun triggerLazySave() { - needsSaving.set(true) - if (isSaving.compareAndSet(false, true)) { - this@AutoOpenSnaps.context.coroutineScope.launch(Dispatchers.IO) { - while (needsSaving.get() && engineActive.get()) { - needsSaving.set(false); saveQueueToDisk(); delay(LAZY_SAVE_INTERVAL_MS) - } - isSaving.set(false) - } + synchronized(queuedSnaps) { queuedSnaps.add(item) } + snapQueue.tryEmit(System.currentTimeMillis()) + + acquireWakeLock(); updateStatusNotification() } } private fun saveQueueToDisk() { - prefs.edit { putInt(PREF_TOTAL_OPENED, totalProcessed.get()); putLong(PREF_SESSION_START, sessionStartTime.get()) } + prefs.edit { + putInt(PREF_TOTAL_OPENED, totalProcessed.get()) + putLong(PREF_SESSION_START, sessionStartTime.get()) + synchronized(queuedSnaps) { putString(PREF_SAVED_QUEUE, gson.toJson(queuedSnaps)) } + } } private fun restorePersistence() { val savedStartTime = prefs.getLong(PREF_SESSION_START, 0) - if (System.currentTimeMillis() - savedStartTime > 21600000) return + val now = System.currentTimeMillis() + if (now - savedStartTime > 21600000) return totalProcessed.set(prefs.getInt(PREF_TOTAL_OPENED, 0)); sessionStartTime.set(savedStartTime) + val savedQueueJson = prefs.getString(PREF_SAVED_QUEUE, null) + if (!savedQueueJson.isNullOrBlank()) { + runCatching { + val restored: List = gson.fromJson(savedQueueJson, object : TypeToken>() {}.type) + synchronized(queuedSnaps) { queuedSnaps.addAll(restored.filter { (now - it.timestamp) < 3600000 }) } + } + } } private fun isWifiConnected(): Boolean { val cm = this@AutoOpenSnaps.context.androidContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - return cm.getNetworkCapabilities(cm.activeNetwork)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true + + // Check for any available network with a WiFi or Ethernet transport + return cm.allNetworks.any { network -> + cm.getNetworkCapabilities(network)?.let { caps -> + caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) + } == true + } } private fun isDeviceIdle(): Boolean = (this@AutoOpenSnaps.context.androidContext.getSystemService(Context.POWER_SERVICE) as PowerManager).isDeviceIdleMode @@ -307,12 +379,21 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A } private fun acquireWakeLock() { + wakeLockCooldownJob?.cancel() if (wakeLock?.isHeld == true) return wakeLock = (this@AutoOpenSnaps.context.androidContext.getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PurrfectSnap:AutoOpen").apply { acquire(8 * 60 * 60 * 1000L) } } private fun releaseWakeLock() { if (wakeLock?.isHeld == true) wakeLock?.release(); wakeLock = null } + private fun startWakeLockCooldown() { + wakeLockCooldownJob?.cancel() + wakeLockCooldownJob = this@AutoOpenSnaps.context.coroutineScope.launch { + delay(30000) + releaseWakeLock() + } + } + private fun createNotificationChannels() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { notificationManager.createNotificationChannel(NotificationChannel("auto_open_status", "Auto-Open Status", NotificationManager.IMPORTANCE_LOW).apply { enableVibration(false); setSound(null, null) }) @@ -321,9 +402,10 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private fun updateStatusNotification(force: Boolean = false) { val now = System.currentTimeMillis() + if (!isScreenOn.get() && !force) return if (!force && (now - lastNotificationUpdate) < notificationUpdateDelay) { if (pendingNotificationUpdate.compareAndSet(false, true)) { - this@AutoOpenSnaps.context.coroutineScope.launch { delay(notificationUpdateDelay - (now - lastNotificationUpdate)); updateStatusNotificationInternal() } + this@AutoOpenSnaps.context.coroutineScope.launch { delay(notificationUpdateDelay - (now - lastNotificationUpdate)); updateStatusNotificationInternal() } } return } @@ -335,31 +417,35 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A val processed = sessionProcessed.get() val total = totalProcessed.get() val remaining = synchronized(queuedSnaps) { queuedSnaps.size } + + // Industrial State Hashing: Prevent redundant redraws and CPU wakeups + val currentStateHash = Objects.hash(processed, total, remaining, currentStatusText, isPaused.get()) + if (currentStateHash == lastNotificationStateHash && remaining == 0) return + lastNotificationStateHash = currentStateHash + val isWorking = remaining > 0 val speed = if (isWorking) getSnapsPerSecond() else 0.0 - + lastNotificationUpdate = System.currentTimeMillis(); pendingNotificationUpdate.set(false) - + val sessionTotal = processed + remaining val progressPercent = if (sessionTotal > 0) (processed * 100) / sessionTotal else 0 val eta = if (isWorking && !isPaused.get()) formatDuration(remaining * averageProcessingTime.get()) else "..." val builder = Notification.Builder(this@AutoOpenSnaps.context.androidContext, "auto_open_status") .setOngoing(isWorking).setOnlyAlertOnce(true).setGroup(NOTIFICATION_GROUP_KEY) - - // ICON LOGIC: Pause, Monitoring (Sync), or Active (Play) - val iconRes = when { - isPaused.get() -> android.R.drawable.ic_media_pause - !isWorking -> android.R.drawable.ic_popup_sync - else -> android.R.drawable.ic_media_play - } - builder.setSmallIcon(iconRes) + + builder.setSmallIcon(if (isPaused.get()) android.R.drawable.ic_media_pause else if (!isWorking) android.R.drawable.ic_popup_sync else android.R.drawable.ic_media_play) builder.setContentTitle("Auto-Open: $currentStatusText") - + val isCompact = (autoOpenConfig.compactNotification as PropertyValue).get() if (isWorking) { builder.setContentText("Opened: $processed │ Queue: $remaining ($progressPercent%)") - builder.setSubText("Speed: ${String.format(Locale.US, "%.1f", speed)}/s • Ends in: $eta") + if (isCompact) { + builder.setSubText("Speed: ${String.format(Locale.US, "%.1f", speed)}/s • Ends in: $eta") + } else { + builder.setSubText("") + } builder.setProgress(sessionTotal, processed, false) } else { builder.setContentText("$processed Opened Today │ $total Total") @@ -383,10 +469,10 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A } val speedNotion = if (isWorking) currentSpeedText else "Idle" val speedValue = "${String.format(Locale.US, "%.1f", speed)}/s" - append("└─ Speed: $speedNotion ($speedValue)\n") + append("└─ Speed: $speedNotion ($speedValue)") if ((autoOpenConfig.showQueuePreview as PropertyValue).get()) { - append("\nQUEUE PREVIEW\n") + append("\n\nQUEUE PREVIEW\n") if (isWorking && remaining > 0) { recentSnaps.reversed().forEach { item -> append("• ${item.senderName} │ ${item.conversationType} (${item.contentType})\n") @@ -400,21 +486,22 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A builder.setStyle(bigTextStyle) } - notificationManager.notify(STATUS_NOTIFICATION_ID, builder.build()) + runCatching { notificationManager.notify(STATUS_NOTIFICATION_ID, builder.build()) }.onFailure { logError("Failed to update notification (System not ready)", it) } } - private fun createPendingIntent(action: String): PendingIntent { val intent = Intent(action).setPackage(this@AutoOpenSnaps.context.androidContext.packageName) return PendingIntent.getBroadcast(this@AutoOpenSnaps.context.androidContext, action.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } private fun setupReceivers() { - val actionReceiver = object : BroadcastReceiver() { + actionReceiver = object : BroadcastReceiver() { override fun onReceive(ctx: Context?, intent: Intent?) { when (intent?.action) { ACTION_PAUSE_RESUME -> { isPaused.set(!isPaused.get()); updateStatusNotification(force = true) } ACTION_CLEAR_QUEUE -> { sessionProcessed.set(0); synchronized(queuedSnaps) { queuedSnaps.clear() }; updateStatusNotification(force = true) } ACTION_STOP_ENGINE -> shutdownFeature() + Intent.ACTION_SCREEN_ON -> { isScreenOn.set(true); updateStatusNotification(force = true) } + Intent.ACTION_SCREEN_OFF -> { isScreenOn.set(false) } Intent.ACTION_BATTERY_CHANGED -> { val temp = intent.getIntExtra("temperature", 0) / 10f if (temp >= 40f && !isThermalThrottled) { isThermalThrottled = true; lastThermalThrottleAt = System.currentTimeMillis() } @@ -423,21 +510,57 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A } } } - val filter = IntentFilter().apply { addAction(ACTION_PAUSE_RESUME); addAction(ACTION_CLEAR_QUEUE); addAction(ACTION_STOP_ENGINE); addAction(Intent.ACTION_BATTERY_CHANGED) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) this@AutoOpenSnaps.context.androidContext.registerReceiver(actionReceiver, filter, Context.RECEIVER_NOT_EXPORTED) - else this@AutoOpenSnaps.context.androidContext.registerReceiver(actionReceiver, filter) + val filter = IntentFilter().apply { + addAction(ACTION_PAUSE_RESUME) + addAction(ACTION_CLEAR_QUEUE) + addAction(ACTION_STOP_ENGINE) + addAction(Intent.ACTION_SCREEN_ON) + addAction(Intent.ACTION_SCREEN_OFF) + addAction(Intent.ACTION_BATTERY_CHANGED) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) this@AutoOpenSnaps.context.androidContext.registerReceiver(actionReceiver!!, filter, Context.RECEIVER_NOT_EXPORTED) + else this@AutoOpenSnaps.context.androidContext.registerReceiver(actionReceiver!!, filter) } private fun recordSpeedTimestamp() { synchronized(snapTimestamps) { snapTimestamps.addLast(System.currentTimeMillis()); if (snapTimestamps.size > 250) snapTimestamps.removeFirst() } } - - private fun shutdownFeature() { + + private fun shutdownFeature() { engineActive.set(false) - snapChannel.close() engineJob?.cancel() - releaseWakeLock() - cancelStatusNotification() + saveQueueToDisk() + + // Permanently disable the feature in settings + autoOpenConfig.globalState = false + this@AutoOpenSnaps.context.coroutineScope.launch { + runCatching { + val field = context::class.java.getDeclaredField("_config").apply { isAccessible = true } + val modConfig = (field.get(context) as Lazy<*>).value as ModConfig + modConfig.writeConfig() + } + } + + // Surgical clean-up: release resources and listeners + actionReceiver?.let { + runCatching { this@AutoOpenSnaps.context.androidContext.unregisterReceiver(it) } + } + actionReceiver = null + wakeLockCooldownJob?.cancel() + + // Grace period for WakeLock release + this@AutoOpenSnaps.context.coroutineScope.launch { + delay(60000) + releaseWakeLock() + } + + // Show final "Stopped" notice + val builder = Notification.Builder(this@AutoOpenSnaps.context.androidContext, "auto_open_status") + .setOngoing(false) + .setSmallIcon(android.R.drawable.ic_menu_close_clear_cancel) + .setContentTitle("Auto-Open") + .setContentText("Auto-Open Engine Disabled. Re-enable in settings.") + + runCatching { notificationManager.notify(STATUS_NOTIFICATION_ID, builder.build()) }.onFailure { logError("Failed to update notification (System not ready)", it) } } - private fun cancelStatusNotification() = notificationManager.cancel(STATUS_NOTIFICATION_ID) fun getInterface(): AutoOpenInterface { @@ -450,7 +573,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private fun getSenderDisplayName(userId: String): String = this@AutoOpenSnaps.context.database.getFriendInfo(userId)?.displayName ?: "Unknown" private fun getConversationType(convId: String, senderId: String): String = if (this@AutoOpenSnaps.context.database.getDMOtherParticipant(convId) != null) "Friend DM" else this@AutoOpenSnaps.context.database.getFeedEntryByConversationId(convId)?.feedDisplayName ?: "Group Chat" - private fun getSnapContentType(type: ContentType?): String = when (type) { ContentType.SNAP -> "Photo/Video"; ContentType.EXTERNAL_MEDIA -> "Media"; else -> "Message" } + private fun getSnapContentType(type: ContentType?): String = when (type) { ContentType.SNAP -> "Photo/Video"; ContentType.EXTERNAL_MEDIA -> "Media"; else -> "Message" } } data class SnapQueueItem(val conversationId: String, val messageId: Long, val serverMessageId: Long, val senderId: String, val senderName: String, val conversationType: String, val contentType: String, val timestamp: Long = System.currentTimeMillis()) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/DeviceSpooferHook.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/DeviceSpooferHook.kt index 00665201..71465d08 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/DeviceSpooferHook.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/DeviceSpooferHook.kt @@ -144,8 +144,24 @@ class DeviceSpooferHook : Feature("Device Spoofer") { } private fun getRandomizedProfile(): RandomizedDeviceProfile { - val generationToken = context.config.experimental.spoof.randomizeDeviceProfile.profileGenerationToken.getNullable() - return randomizedProfile ?: RandomizedDeviceProfileStore + if (randomizedProfile != null) return randomizedProfile!! + + val spoofConfig = context.config.experimental.spoof.randomizeDeviceProfile + val configProfileJson = spoofConfig.profileData.getNullable() + + if (!configProfileJson.isNullOrBlank()) { + runCatching { + val profile = RandomizedDeviceProfile.fromJson(configProfileJson) + randomizedProfile = profile + context.log.verbose("Using restored randomized device profile from config") + return profile + }.onFailure { + context.log.warn("Failed to parse restored device profile from config, generating fresh one: ${it.message}") + } + } + + val generationToken = spoofConfig.profileGenerationToken.getNullable() + return RandomizedDeviceProfileStore .getOrCreate(context.androidContext, context.log, generationToken) .also { profile -> randomizedProfile = profile @@ -155,18 +171,23 @@ class DeviceSpooferHook : Feature("Device Spoofer") { private fun persistRandomizedProfileSnapshot(profile: RandomizedDeviceProfile) { val spoofConfig = context.config.experimental.spoof.randomizeDeviceProfile + val profileJson = profile.toJson().toString() val snapshot = profile.toJson().toString(2) - if (spoofConfig.currentProfileSnapshot.getNullable() == snapshot) return + + if (spoofConfig.currentProfileSnapshot.getNullable() == snapshot && spoofConfig.profileData.getNullable() == profileJson) return + spoofConfig.currentProfileSnapshot.set(snapshot) + spoofConfig.profileData.set(profileJson) // Synchronize raw profile data for multi-process persistence + runCatching { val field = context.javaClass.getDeclaredField("_config\$delegate") field.isAccessible = true val lazyConfig = field.get(context) as Lazy<*> val modConfig = lazyConfig.value as? ModConfig ?: return@runCatching modConfig.writeConfig(dispatchConfigListener = false) - context.log.verbose("Persisted randomized device profile snapshot to config") + context.log.verbose("Persisted randomized device profile data to config") }.onFailure { - context.log.warn("Failed to persist randomized device profile snapshot: ${it.message}") + context.log.warn("Failed to persist randomized device profile: ${it.message}") } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt index 391b1043..4e60c178 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt @@ -70,33 +70,37 @@ class MediaFilePicker : Feature("Media File Picker") { private const val SNAP_CHUNK_DURATION_MS = 10_000L private val queuedSplitItems = ArrayDeque() private val queuedSplitItemIds = ArrayDeque() - private val queuedSplitCleanupUris = mutableMapOf() + private val queuedSplitCleanupItems = mutableMapOf() private var originalUnsplitItem: Any? = null private var reusableOriginalItem: Any? = null private var queuedOverrideType: String? = null + private var queuedOverrideSnapDurationMs: Int? = null private var bypassSplitOnce = false private var sendSingleItemHandler: ((Any) -> Boolean)? = null - private var cleanupItemHandler: ((String) -> Unit)? = null + private var cleanupItemHandler: ((String, String?) -> Unit)? = null fun hasQueuedSplitItems(): Boolean = queuedSplitItems.isNotEmpty() fun hasPendingSplitCleanup(): Boolean = queuedSplitItemIds.isNotEmpty() fun hasOriginalUnsplitItem(): Boolean = originalUnsplitItem != null fun hasReusableOriginalItem(): Boolean = reusableOriginalItem != null - fun setQueuedOverrideType(value: String?) { + fun setQueuedOverrideType(value: String?, snapDurationMs: Int? = 10_000) { queuedOverrideType = value + queuedOverrideSnapDurationMs = if (value == null) null else snapDurationMs } fun getQueuedOverrideType(): String? = queuedOverrideType + fun getQueuedOverrideSnapDurationMs(): Int? = queuedOverrideSnapDurationMs fun clearQueuedSplitItems(deleteTempItems: Boolean = true) { if (deleteTempItems) { val cleanup = cleanupItemHandler - queuedSplitCleanupUris.values.toList().forEach { uri -> - cleanup?.invoke(uri) + queuedSplitCleanupItems.values.toList().forEach { item -> + cleanup?.invoke(item.uri, item.filePath) } } queuedSplitItems.clear() queuedSplitItemIds.clear() - queuedSplitCleanupUris.clear() + queuedSplitCleanupItems.clear() originalUnsplitItem = null queuedOverrideType = null + queuedOverrideSnapDurationMs = null } fun sendReusableOriginalItem(): Boolean { val item = reusableOriginalItem ?: return false @@ -110,30 +114,34 @@ class MediaFilePicker : Feature("Media File Picker") { items.drop(1).forEach { queuedSplitItems.addLast(it) } preparedItems.forEach { queuedSplitItemIds.addLast(it.itemId) - queuedSplitCleanupUris[it.itemId] = it.uri + queuedSplitCleanupItems[it.itemId] = it } } fun sendOriginalUnsplitItem(): Boolean { val item = originalUnsplitItem ?: return false val overrideType = queuedOverrideType + val overrideSnapDurationMs = queuedOverrideSnapDurationMs clearQueuedSplitItems(deleteTempItems = true) queuedOverrideType = overrideType + queuedOverrideSnapDurationMs = overrideSnapDurationMs bypassSplitOnce = true val sender = sendSingleItemHandler ?: return false return sender(item) } fun handleCurrentQueuedItemSuccess(): Boolean { queuedSplitItemIds.removeFirstOrNull()?.let { itemId -> - queuedSplitCleanupUris.remove(itemId)?.let { uri -> - cleanupItemHandler?.invoke(uri) + queuedSplitCleanupItems.remove(itemId)?.let { item -> + cleanupItemHandler?.invoke(item.uri, item.filePath) } } if (queuedSplitItems.isEmpty()) { queuedOverrideType = null + queuedOverrideSnapDurationMs = null return false } val next = queuedSplitItems.removeFirstOrNull() ?: run { queuedOverrideType = null + queuedOverrideSnapDurationMs = null return false } val sender = sendSingleItemHandler ?: return false @@ -151,7 +159,8 @@ class MediaFilePicker : Feature("Media File Picker") { private data class PreparedMediaItem( val itemId: String, val durationMs: Long, - val uri: String + val uri: String, + val filePath: String? = null ) private fun splitVideoIntoChunks( @@ -301,7 +310,12 @@ class MediaFilePicker : Feature("Media File Picker") { runCatching { resolver.delete(uri, null, null) } } - return PreparedMediaItem(itemId = itemId, durationMs = durationMs, uri = uri.toString()) + return PreparedMediaItem( + itemId = itemId, + durationMs = durationMs, + uri = uri.toString(), + filePath = file.absolutePath + ) } private fun buildDrawerItems(itemClass: Any, mediaItems: List): List { @@ -423,11 +437,18 @@ class MediaFilePicker : Feature("Media File Picker") { false } } - cleanupItemHandler = { uriString -> + cleanupItemHandler = { uriString, filePath -> runCatching { + // Industrial Cleanup: Direct file deletion is the gold standard for Android 14 + filePath?.let { path -> + val file = File(path) + if (file.exists()) file.delete() + } context.androidContext.contentResolver.delete(Uri.parse(uriString), null, null) }.onFailure { - context.log.warn("MediaFilePicker: Failed to delete temp split media: ${it.message}") + if (it.message?.contains("no access") == false) { + context.log.warn("MediaFilePicker: Failed to delete temp split media: ${it.message}") + } } } if (sendItemsHookedHandler === handlerInstance) return@hook diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/AdBlockFix.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/AdBlockFix.kt index c630f242..8cb2e4e4 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/AdBlockFix.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/AdBlockFix.kt @@ -2,7 +2,6 @@ package me.eternal.purrfectsnap.core.features.impl.global import android.os.SystemClock import android.view.View -import me.eternal.purrfectsnap.core.event.events.impl.BindViewEvent import me.eternal.purrfectsnap.core.features.Feature import me.eternal.purrfectsnap.core.ui.hideViewCompletely import me.eternal.purrfectsnap.core.ui.dispatchSyntheticTap @@ -35,7 +34,6 @@ class AdBlockFix : Feature("AdBlockFix") { hookFeedEntryTracking() hookMessagingFeedCallbacks() - hookChatFeedRowSuppression() hookOperaAutoSkip() } @@ -45,7 +43,7 @@ class AdBlockFix : Feature("AdBlockFix") { val conversationId = feedEntry.getObjectFieldOrNull("mConversationId")?.let(::SnapUUID)?.toString() ?: return@hookConstructor - if (isCampaignFeedEntry(feedEntry) || isChatAdShareFeedEntry(feedEntry)) { + if (isCampaignFeedEntry(feedEntry)) { adConversationIds.add(conversationId) } } @@ -123,36 +121,20 @@ class AdBlockFix : Feature("AdBlockFix") { } } - private fun hookChatFeedRowSuppression() { - context.event.subscribe(BindViewEvent::class) { event -> - val modelDump = event.prevModel.toString() - event.friendFeedItem { conversationId -> - if (adConversationIds.contains(conversationId) || isChatAdShareModel(modelDump)) { - hideBoundChatFeedRow(event.view) - } - } - } - } - - private fun hideBoundChatFeedRow(view: View) { - view.hideViewCompletely() - (view.parent as? View)?.hideViewCompletely() - (view.parent?.parent as? View)?.hideViewCompletely() - } - private fun hookOperaAutoSkip() { onNextActivityCreate { context.mappings.useMapper(OperaPageViewControllerMapper::class) { arrayOf(onDisplayStateChange, onDisplayStateChangeGesture).forEach { methodName -> val resolvedMethod = methodName.get() ?: return@forEach classReference.get()?.hook(resolvedMethod, HookStage.AFTER) { param -> + val instance = param.thisObject() val viewState = runCatching { - param.thisObject().getObjectField(viewStateField.get()!!)?.toString() + instance::class.java.methods.firstOrNull { it.name.contains("ViewState") || it.name == "g" }?.invoke(instance)?.toString() }.getOrNull() ?: return@hook if (viewState != "FULLY_DISPLAYED") return@hook val layerList = runCatching { - param.thisObject().getObjectField(layerListField.get()!!) as? ArrayList<*> + instance::class.java.methods.firstOrNull { it.name.contains("LayerList") || it.name == "l" }?.invoke(instance) as? ArrayList<*> }.getOrNull() ?: return@hook val paramMap = runCatching { layerList.map { Layer(it).paramMap }.firstOrNull() @@ -209,25 +191,6 @@ class AdBlockFix : Feature("AdBlockFix") { ?.getObjectFieldOrNull("mCampaignMetadata") != null } - private fun isChatAdShareFeedEntry(feedEntry: Any): Boolean { - val interactionDump = feedEntry.getObjectFieldOrNull("mInteractionInfo")?.toString().orEmpty() - val displayDump = feedEntry.getObjectFieldOrNull("mDisplayInfo")?.toString().orEmpty() - val combined = "$interactionDump $displayDump" - return isChatAdShareModel(combined) - } - - private fun isChatAdShareModel(modelDump: String): Boolean { - if (modelDump.isBlank()) return false - return modelDump.contains("CHAT_AD_SHARE") || - modelDump.contains("AD_SHARE") || - modelDump.contains("ChatAd") || - modelDump.contains("chat_ad_share") || - modelDump.contains("chat_sponsored_snap") || - modelDump.contains("CommonAttachmentViewModel") || - modelDump.contains("visibilityFeedbackURL") || - modelDump.contains("pageLoadPingURL") - } - private fun isSpotlightCommercialPage(paramMap: ParamMap): Boolean { val snapSource = paramMap["SNAP_SOURCE"]?.toString() if (snapSource != "SINGLE_SNAP_STORY" && snapSource != "SPOTLIGHT" && snapSource != "PUBLIC_STORY") { diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/SnapchatPlus.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/SnapchatPlus.kt index aa5fbb98..f6cddd77 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/SnapchatPlus.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/SnapchatPlus.kt @@ -17,67 +17,59 @@ class SnapchatPlus: Feature("SnapchatPlus") { override fun init() { val snapchatPlusTier = context.config.global.snapchatPlus.getNullable() + if (snapchatPlusTier == null || snapchatPlusTier == "not_subscribed") return - if (snapchatPlusTier != null) { - context.mappings.useMapper(PlusSubscriptionMapper::class) { - classReference.get()?.hookConstructor(HookStage.AFTER) { param -> - param.thisObject().dataBuilder { - //subscription tier - if (get(tierField.getAsString()!!)?.javaClass?.isEnum == true) { - set(tierField.getAsString()!!, when (snapchatPlusTier) { - "not_subscribed" -> "NO_ACCESS" - "basic" -> "SNAPCHAT_PLUS" - "ad_free" -> "SNAPCHAT_PLUS_AD_FREE" - else -> "SNAPCHAT_PLUS" - }) - } else { - set(tierField.getAsString()!!, when (snapchatPlusTier) { - "not_subscribed" -> 1 - "basic" -> 2 - "ad_free" -> 3 - else -> 2 - }) - } + // Pre-calculate custom purchase date to eliminate main thread lag + val customPurchaseDateRaw = context.config.global.snapchatPlusPurchaseDate.get().trim() + val customPurchaseDateMillis = if (customPurchaseDateRaw.isNotEmpty()) { + runCatching { + LocalDate.parse(customPurchaseDateRaw, DateTimeFormatter.ISO_LOCAL_DATE) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli() + }.getOrNull() + } else (System.currentTimeMillis() - 7776000000L) // 3 months fallback - //subscription status - set(statusField.getAsString()!!, 2) - - val fallbackOriginalSubscriptionTime = System.currentTimeMillis() - 7776000000L - val customPurchaseDate = context.config.global.snapchatPlusPurchaseDate.get().trim() - val customPurchaseDateMillis = if (customPurchaseDate.isNotEmpty()) { - runCatching { - LocalDate - .parse(customPurchaseDate, DateTimeFormatter.ISO_LOCAL_DATE) - .atStartOfDay(ZoneId.systemDefault()) - .toInstant() - .toEpochMilli() - }.getOrNull() - } else { - null - } - - set( - originalSubscriptionTimeMillisField.getAsString()!!, - customPurchaseDateMillis ?: fallbackOriginalSubscriptionTime - ) - set(expirationTimeMillisField.getAsString()!!, expirationTimeMillis) + context.mappings.useMapper(PlusSubscriptionMapper::class) { + classReference.get()?.hookConstructor(HookStage.AFTER) { param -> + param.thisObject().dataBuilder { + //subscription tier + if (get(tierField.getAsString()!!)?.javaClass?.isEnum == true) { + set(tierField.getAsString()!!, when (snapchatPlusTier) { + "not_subscribed" -> "NO_ACCESS" + "basic" -> "SNAPCHAT_PLUS" + "ad_free" -> "SNAPCHAT_PLUS_AD_FREE" + else -> "SNAPCHAT_PLUS" + }) + } else { + set(tierField.getAsString()!!, when (snapchatPlusTier) { + "not_subscribed" -> 1 + "basic" -> 2 + "ad_free" -> 3 + else -> 2 + }) } + + //subscription status + set(statusField.getAsString()!!, 2) + + set( + originalSubscriptionTimeMillisField.getAsString()!!, + customPurchaseDateMillis + ) + set(expirationTimeMillisField.getAsString()!!, expirationTimeMillis) } } } + // Force enable all premium features in the catalog if (context.config.experimental.hiddenSnapchatPlusFeatures.get()) { - findClass("com.snap.plus.FeatureCatalog").methods.last { - !it.name.contains("init") && - it.parameterTypes.isNotEmpty() && - it.parameterTypes[0].name != "java.lang.Boolean" - }.hook(HookStage.BEFORE) { param -> - val instance = param.thisObject() - val firstArg = param.argNullable(0) ?: return@hook - - instance.findFieldNamesByType(firstArg::class.java).forEach { fieldName -> - instance.setObjectField(fieldName, firstArg) + runCatching { + val featureCatalogClass = findClass("com.snap.plus.FeatureCatalog") + featureCatalogClass.hook("isFeatureEnabled", HookStage.BEFORE) { param -> + param.setResult(true) } + context.log.verbose("Successfully unlocked premium Snapchat features") } } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt index 71beb462..6d89ab00 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt @@ -1,7 +1,13 @@ package me.eternal.purrfectsnap.core.features.impl.messaging +import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.os.Build import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background @@ -28,10 +34,12 @@ import androidx.compose.ui.text.input.KeyboardType import kotlinx.coroutines.* import me.eternal.purrfectsnap.bridge.task.TaskListener import me.eternal.purrfectsnap.common.data.ContentType +import me.eternal.purrfectsnap.common.config.PropertyValue import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog import me.eternal.purrfectsnap.common.util.protobuf.ProtoEditor import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader import me.eternal.purrfectsnap.common.util.protobuf.ProtoWriter +import me.eternal.purrfectsnap.core.ModContext import me.eternal.purrfectsnap.core.event.events.impl.MediaUploadEvent import me.eternal.purrfectsnap.core.event.events.impl.NativeUnaryCallEvent import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent @@ -64,38 +72,126 @@ import kotlin.time.toDuration class SendOverride : Feature("Send Override") { companion object { private const val NOTIFICATION_CHANNEL_ID = "scheduled_send" + private const val CONTINUOUS_SEND_CHANNEL_ID = "continuous_send_status" + private const val STATUS_NOTIFICATION_ID = 54322 + private const val COMPLETION_NOTIFICATION_ID = 54323 + + const val ACTION_PAUSE_RESUME = "me.eternal.purrfectsnap.CONTINUOUS_SEND_PAUSE_RESUME" + const val ACTION_STOP = "me.eternal.purrfectsnap.CONTINUOUS_SEND_STOP" + private val internalMultipartSend = ThreadLocal.withInitial { false } private var queuedOriginalItemRepeatCount = 0 private var queuedOriginalItemRepeatOverrideType: String? = null + private var queuedOriginalItemRepeatSnapDurationMs: Int? = null - private fun queueOriginalItemRepeats(repeatCount: Int, overrideType: String) { + // Notification & Loop Tracking + private var totalRepeatCount = 0 + private var processedRepeatCount = 0 + private var currentRecipientName: String = "Unknown" + private val isPaused = java.util.concurrent.atomic.AtomicBoolean(false) + private val isStopped = java.util.concurrent.atomic.AtomicBoolean(false) + private val engineActive = java.util.concurrent.atomic.AtomicBoolean(true) + + private fun queueOriginalItemRepeats(repeatCount: Int, overrideType: String, snapDurationMs: Int?) { queuedOriginalItemRepeatCount = repeatCount queuedOriginalItemRepeatOverrideType = overrideType - MediaFilePicker.setQueuedOverrideType(overrideType) + queuedOriginalItemRepeatSnapDurationMs = snapDurationMs + MediaFilePicker.setQueuedOverrideType(overrideType, snapDurationMs) } private fun clearQueuedOriginalItemRepeats() { queuedOriginalItemRepeatCount = 0 queuedOriginalItemRepeatOverrideType = null + queuedOriginalItemRepeatSnapDurationMs = null } - private fun handleQueuedOriginalItemRepeatSuccess(): Boolean { - if (queuedOriginalItemRepeatCount <= 0) { + private fun updateContinuousSendNotification(context: ModContext) { + if (!engineActive.get()) return + + val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java) + val remaining = queuedOriginalItemRepeatCount + val processed = processedRepeatCount + val total = totalRepeatCount + val isWorking = remaining > 0 && !isStopped.get() && engineActive.get() + + if (!isWorking) { + notificationManager.cancel(STATUS_NOTIFICATION_ID) + showCompletionNotification(context, processed, total) + return + } + + val progressPercent = if (total > 0) (processed * 100) / total else 0 + + val builder = Notification.Builder(context.androidContext, CONTINUOUS_SEND_CHANNEL_ID) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setSmallIcon(android.R.drawable.ic_popup_sync) + .setColor(0xFF3498DB.toInt()) + .setContentTitle("Sending Snaps to $currentRecipientName") + .setContentText("Progress: $processed / $total ($progressPercent%)") + .setSubText("$processed / $total") + .setProgress(total, processed, false) + + val pauseResumeLabel = if (isPaused.get()) "Resume" else "Pause" + builder.addAction(Notification.Action.Builder(null, pauseResumeLabel, createPendingIntent(context, ACTION_PAUSE_RESUME)).build()) + builder.addAction(Notification.Action.Builder(null, "Stop", createPendingIntent(context, ACTION_STOP)).build()) + + notificationManager.notify(STATUS_NOTIFICATION_ID, builder.build()) + } + + private fun showCompletionNotification(context: ModContext, sent: Int, total: Int) { + val isError = sent < total && !isStopped.get() + val title = when { + isStopped.get() -> "Continuous Send Stopped" + isError -> "Continuous Send Failed" + else -> "Continuous Send Finished" + } + val content = "Sent $sent / $total snaps to $currentRecipientName" + + val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java) + val builder = Notification.Builder(context.androidContext, CONTINUOUS_SEND_CHANNEL_ID) + .setSmallIcon(if (isError) android.R.drawable.stat_notify_error else android.R.drawable.checkbox_on_background) + .setColor(if (isError) 0xFFE74C3C.toInt() else 0xFF2ECC71.toInt()) + .setContentTitle(title) + .setContentText(content) + .setAutoCancel(true) + + notificationManager.notify(COMPLETION_NOTIFICATION_ID, builder.build()) + } + + private fun createPendingIntent(context: ModContext, action: String): PendingIntent { + val intent = Intent(action).setPackage(context.androidContext.packageName) + return PendingIntent.getBroadcast( + context.androidContext, + action.hashCode(), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + + private fun handleQueuedOriginalItemRepeatSuccess(context: ModContext): Boolean { + if (isStopped.get() || queuedOriginalItemRepeatCount <= 0) { clearQueuedOriginalItemRepeats() + updateContinuousSendNotification(context) return false } val overrideType = queuedOriginalItemRepeatOverrideType ?: run { clearQueuedOriginalItemRepeats() + updateContinuousSendNotification(context) return false } + val snapDurationMs = queuedOriginalItemRepeatSnapDurationMs + processedRepeatCount++ queuedOriginalItemRepeatCount-- - MediaFilePicker.setQueuedOverrideType(overrideType) + updateContinuousSendNotification(context) + + MediaFilePicker.setQueuedOverrideType(overrideType, snapDurationMs) val result = MediaFilePicker.sendReusableOriginalItem() if (!result) { - queuedOriginalItemRepeatCount++ clearQueuedOriginalItemRepeats() + updateContinuousSendNotification(context) } return result } @@ -112,6 +208,20 @@ class SendOverride : Feature("Send Override") { private val backgroundHookLock = Any() private var backgroundHookRefs = 0 private var backgroundHooks: List? = null + + private fun createContinuousNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java) + val channel = NotificationChannel( + CONTINUOUS_SEND_CHANNEL_ID, + "Continuous Send", + NotificationManager.IMPORTANCE_LOW + ) + channel.description = "Progress status for continuous snap sending" + notificationManager.createNotificationChannel(channel) + } + } + private fun acquireScheduledSendBackground(): () -> Unit { if (!context.config.messaging.scheduledSendAllowRunningInBackground.get()) return {} var enableFailed = false @@ -192,7 +302,35 @@ class SendOverride : Feature("Send Override") { @OptIn(ExperimentalLayoutApi::class) override fun init() { createNotificationChannel() - + createContinuousNotificationChannel() + + val actionReceiver = object : BroadcastReceiver() { + override fun onReceive(ctx: Context?, intent: Intent?) { + when (intent?.action) { + ACTION_PAUSE_RESUME -> { + isPaused.set(!isPaused.get()) + updateContinuousSendNotification(context) + } + ACTION_STOP -> { + isStopped.set(true) + if (isPaused.get()) { + isPaused.set(false) + } + updateContinuousSendNotification(context) + } + } + } + } + val filter = IntentFilter().apply { + addAction(ACTION_PAUSE_RESUME) + addAction(ACTION_STOP) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.androidContext.registerReceiver(actionReceiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + context.androidContext.registerReceiver(actionReceiver, filter) + } + val stripMediaMetadata = context.config.messaging.stripMediaMetadata.get() var postSavePolicy: Int? = null @@ -727,10 +865,10 @@ class SendOverride : Feature("Send Override") { completionCallback: Any? ): Boolean { val sourceReader = ProtoReader(sourceMessageContent.content ?: return false) - val mediaCount = sourceReader.followPath(3)?.getCount(3) ?: 0 + val mediaCount = (sourceReader.followPath(3) as? ProtoReader)?.getCount(3) ?: 0 if (overrideType != "ORIGINAL" && mediaCount > 1) { val mediaBuffers = mutableListOf() - sourceReader.followPath(3)?.eachBuffer { id, buffer -> + (sourceReader.followPath(3) as? ProtoReader)?.eachBuffer { id, buffer -> if (id == 3) mediaBuffers.add(buffer) } if (mediaBuffers.isEmpty()) return false @@ -802,22 +940,54 @@ class SendOverride : Feature("Send Override") { if (repeatCount <= 0) return false fun sendIteration(index: Int) { - val callback = if (index == repeatCount - 1) { - originalCallback - } else { - CallbackBuilder(sendMessageCallbackClass) + context.coroutineScope.launch { + while (isPaused.get() && !isStopped.get()) { + delay(500) + } + if (isStopped.get()) { + clearQueuedOriginalItemRepeats() + updateContinuousSendNotification(context) + return@launch + } + + val callback = CallbackBuilder(sendMessageCallbackClass) .override("onSuccess") { - sendIteration(index + 1) + processedRepeatCount++ + queuedOriginalItemRepeatCount-- + updateContinuousSendNotification(context) + + if (index < repeatCount - 1) { + sendIteration(index + 1) + } else { + // Batch Finished: Trigger original Snapchat callback + originalCallback?.let { cb -> + runCatching { + val method = cb.javaClass.methods.firstOrNull { it.name == "onSuccess" } + if (method != null) { + if (method.parameterCount == 0) { + method.invoke(cb) + } else { + // Pass null for all required parameters to safely trigger the completion UI + method.invoke(cb, *arrayOfNulls(method.parameterCount)) + } + } + }.onFailure { context.log.error("Failed to trigger completion handshake", it) } + } + } } .override("onError", shouldUnhook = false) { invokeCallbackError(originalCallback, it.argNullable(0)) + clearQueuedOriginalItemRepeats() + updateContinuousSendNotification(context) } .build() - } - val preparedContent = createMessageContentFromOriginal() - if (!sendMediaManual(preparedContent, overrideType, snapDurationMs, callback)) { - invokeCallbackError(originalCallback, "Failed to send") + if (index > 0) delay(1000) // Human-like delay + + val preparedContent = createMessageContentFromOriginal() + if (!sendMediaManual(preparedContent, overrideType, snapDurationMs, callback)) { + invokeCallbackError(originalCallback, "Failed to send") + } } } @@ -830,15 +1000,21 @@ class SendOverride : Feature("Send Override") { return applyOverride(localMessageContent, messageProtoReader, overrideType, snapDurationMs) } - val resolvedOverrideType = MediaFilePicker.getQueuedOverrideType() + val queuedOverrideType = MediaFilePicker.getQueuedOverrideType() + val resolvedOverrideType = queuedOverrideType ?: configOverrideType?.takeIf { it != "always_ask" } + val resolvedSnapDurationMs = if (queuedOverrideType != null) { + MediaFilePicker.getQueuedOverrideSnapDurationMs() + } else { + 10000 + } fun attachQueuedRepeatCallbacks(sendEvent: SendMessageWithContentEvent) { sendEvent.addCallbackResult("onSuccess") { context.runOnUiThread { val handledSplit = MediaFilePicker.handleCurrentQueuedItemSuccess() val handledRepeat = if (!handledSplit) { - handleQueuedOriginalItemRepeatSuccess() + handleQueuedOriginalItemRepeatSuccess(context) } else { false } @@ -858,7 +1034,7 @@ class SendOverride : Feature("Send Override") { if (MediaFilePicker.hasPendingSplitCleanup() || MediaFilePicker.getQueuedOverrideType() != null || queuedOriginalItemRepeatCount > 0) { attachQueuedRepeatCallbacks(event) } - if (sendMedia(resolvedOverrideType, 10000)) { + if (sendMedia(resolvedOverrideType, resolvedSnapDurationMs)) { if (event.canceled) invokeOriginalAndRestoreResult(event) } return@subscribe @@ -866,7 +1042,6 @@ class SendOverride : Feature("Send Override") { context.runOnUiThread { val recipientNameForTask = recipientName - val mediaCount = messageProtoReader.followPath(3)?.getCount(3) ?: 0 createComposeAlertDialog(context.mainActivity!!) { alertDialog -> PurrfectOverlayTheme { @@ -1274,6 +1449,11 @@ class SendOverride : Feature("Send Override") { } Button(onClick = { val finalSelectedType = selectedType + val selectedSnapDurationMs = if (finalSelectedType != "SAVEABLE_SNAP") { + convertDuration(customDuration) + } else { + null + } val repeatCount = if (continuousSendEnabled) { continuousSendCount.toIntOrNull()?.takeIf { it > 0 } } else { @@ -1295,13 +1475,13 @@ class SendOverride : Feature("Send Override") { } alertDialog.dismiss() if (disableSplitForCurrentSend && MediaFilePicker.hasOriginalUnsplitItem()) { - MediaFilePicker.setQueuedOverrideType(finalSelectedType) + MediaFilePicker.setQueuedOverrideType(finalSelectedType, selectedSnapDurationMs) if (!MediaFilePicker.sendOriginalUnsplitItem()) { MediaFilePicker.setQueuedOverrideType(null) } return@Button } else if (MediaFilePicker.hasPendingSplitCleanup()) { - MediaFilePicker.setQueuedOverrideType(finalSelectedType) + MediaFilePicker.setQueuedOverrideType(finalSelectedType, selectedSnapDurationMs) event.addCallbackResult("onSuccess") { context.runOnUiThread { if (!MediaFilePicker.handleCurrentQueuedItemSuccess()) { @@ -1361,7 +1541,7 @@ class SendOverride : Feature("Send Override") { if (sendRepeatedMediaManual( repeatCount, finalSelectedType, - if (finalSelectedType != "SAVEABLE_SNAP") convertDuration(customDuration) else null + selectedSnapDurationMs )) { val successText = context.translation.format("schedule_sent_to", "name" to recipientNameForTask) ?: "Sent to $recipientNameForTask" context.inAppOverlay.showStatusToast( @@ -1410,22 +1590,32 @@ class SendOverride : Feature("Send Override") { } } else { if (repeatCount == 1) { - if (sendMedia(finalSelectedType, if (finalSelectedType != "SAVEABLE_SNAP") convertDuration(customDuration) else null)) { + if (sendMedia(finalSelectedType, selectedSnapDurationMs)) { invokeOriginalAndRestoreResult(event) } } else if (MediaFilePicker.hasReusableOriginalItem()) { - queueOriginalItemRepeats(repeatCount - 1, finalSelectedType) + totalRepeatCount = repeatCount + processedRepeatCount = 1 + currentRecipientName = recipientNameForTask + + queueOriginalItemRepeats(repeatCount - 1, finalSelectedType, selectedSnapDurationMs) attachQueuedRepeatCallbacks(event) - if (sendMedia(finalSelectedType, if (finalSelectedType != "SAVEABLE_SNAP") convertDuration(customDuration) else null)) { + if (sendMedia(finalSelectedType, selectedSnapDurationMs)) { invokeOriginalAndRestoreResult(event) } else { clearQueuedOriginalItemRepeats() + updateContinuousSendNotification(context) } } else { + totalRepeatCount = repeatCount + processedRepeatCount = 0 + queuedOriginalItemRepeatCount = repeatCount + currentRecipientName = recipientNameForTask + sendRepeatedMediaManual( repeatCount, finalSelectedType, - if (finalSelectedType != "SAVEABLE_SNAP") convertDuration(customDuration) else null + selectedSnapDurationMs ) } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/MessageLogger.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/MessageLogger.kt index 7cd662b5..3a9d4feb 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/MessageLogger.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/MessageLogger.kt @@ -146,13 +146,17 @@ class MessageLogger : MessagingRuleFeature("MessageLogger", MessagingRuleType.ME it.messageId = uniqueMessageIdentifier it.conversationId = conversationId it.userId = event.message.senderId.toString() - it.username = usernameCache.getOrPut(it.userId) { - context.database.getFriendInfo(it.userId)?.mutableUsername ?: it.userId - } + it.username = usernameCache[it.userId] + ?: context.database.getFriendInfo(it.userId)?.mutableUsername?.also { resolvedUsername -> + usernameCache[it.userId] = resolvedUsername + } + ?: it.userId it.sendTimestamp = event.message.messageMetadata?.createdAt ?: System.currentTimeMillis() - it.groupTitle = groupTitleCache.getOrPut(conversationId) { - context.database.getFeedEntryByConversationId(conversationId)?.feedDisplayName ?: conversationId - } + it.groupTitle = groupTitleCache[conversationId] + ?: context.database.getFeedEntryByConversationId(conversationId)?.feedDisplayName?.also { resolvedGroupTitle -> + groupTitleCache[conversationId] = resolvedGroupTitle + } + ?: conversationId it.messageData = context.gson.toJson(messageInstance).toByteArray(Charsets.UTF_8) } ) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/PerformanceMode.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/PerformanceMode.kt index 71b43ff2..8d484da2 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/PerformanceMode.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/PerformanceMode.kt @@ -3,84 +3,36 @@ package me.eternal.purrfectsnap.core.features.impl.tweaks import android.animation.ValueAnimator import android.app.Activity import android.app.Dialog -import android.content.Context -import android.database.Cursor -import android.database.MatrixCursor import android.database.sqlite.SQLiteDatabase +import android.hardware.camera2.CaptureRequest import android.media.MediaRecorder +import android.os.Build import android.os.HandlerThread import android.os.Process -import android.util.Base64 +import android.transition.Transition +import android.util.Range import android.view.View +import android.view.TextureView +import android.view.ViewPropertyAnimator +import android.view.WindowManager +import android.view.animation.Animation import android.widget.OverScroller import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager -import java.io.File -import java.lang.Thread -import java.lang.reflect.Method -import java.util.LinkedHashMap -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicLong -import java.util.concurrent.ThreadPoolExecutor -import com.google.gson.reflect.TypeToken import me.eternal.purrfectsnap.core.features.Feature -import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging import me.eternal.purrfectsnap.core.event.events.impl.NetworkApiRequestEvent -import me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID -import me.eternal.purrfectsnap.mapper.impl.CallbackMapper import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.findRestrictedMethod import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hookConstructor -import me.eternal.purrfectsnap.core.util.ktx.getObjectField -import me.eternal.purrfectsnap.core.util.ktx.setObjectField import okhttp3.Dispatcher +import java.lang.Thread +import java.lang.reflect.Method +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.atomic.AtomicBoolean class PerformanceMode : Feature("Performance Mode") { - companion object { - private const val CHAT_FEED_CACHE_MAX_ROWS = 400 - private const val CHAT_FEED_CACHE_MAX_BLOB_BYTES = 512 - private const val CHAT_FEED_CACHE_MIN_REFRESH_INTERVAL_MS = 15_000L - private const val CHAT_FEED_CACHE_MAX_AGE_MS = 5L * 60L * 1000L - private const val CHAT_FEED_CACHE_SCHEMA_VERSION = 2 - private const val MESSAGE_WINDOW_STATE_MAX_CONVERSATIONS = 64 - private const val MESSAGE_WINDOW_STATE_MAX_AGE_MS = 7L * 24L * 60L * 60L * 1000L - private const val SNAP_PREFETCH_GROUP_MESSAGES = 48 - private const val SNAP_PREFETCH_DM_MESSAGES = 24 - private const val REOPEN_WARMUP_GROUP_MESSAGES = 160 - private const val REOPEN_WARMUP_DM_MESSAGES = 96 - } - - private data class SnapshotCell( - val type: Int, - val stringValue: String? = null, - val longValue: Long? = null, - val doubleValue: Double? = null, - val blobValue: String? = null, - ) - - private data class CursorSnapshot( - val columns: List, - val rows: List>, - ) - - private data class ChatFeedSnapshotCache( - val schemaVersion: Int, - val queryKey: String, - val createdAt: Long, - val snapshot: CursorSnapshot, - ) - - private data class MessageWindowState( - val conversationId: String, - val currentSize: Int, - val oldestOrderKey: Long?, - val newestOrderKey: Long?, - val updatedAt: Long, - val isGroup: Boolean, - ) - override fun init() { val profile = context.config.global.performanceMode.profile.getNullable() ?: return val isMaxProfile = profile == "max" @@ -90,7 +42,6 @@ class PerformanceMode : Feature("Performance Mode") { Process.THREAD_PRIORITY_MORE_FAVORABLE } val minimumFrameRate = if (isMaxProfile) 60 else 45 - val minimumRecordingFrameRate = if (isMaxProfile) 30 else 24 val durationScale = if (isMaxProfile) 0.35f else 0.55f val recyclerViewCacheSize = if (isMaxProfile) 64 else 32 val maxRequests = if (isMaxProfile) 192 else 96 @@ -100,55 +51,19 @@ class PerformanceMode : Feature("Performance Mode") { val maxAnimationDurationMs = if (isMaxProfile) 90L else 140L val maxScrollDurationMs = if (isMaxProfile) 72 else 180 val preferredRefreshRate = if (isMaxProfile) 120f else 90f - val snapMapTransitionDurationMs = if (isMaxProfile) 0L else 24L - val snapMapCameraDurationMs = if (isMaxProfile) 16L else 64L - val snapMapMoveDurationMs = if (isMaxProfile) 8L else 40L - val snapMapPrefetchZoomDelta = if (isMaxProfile) 6 else 3 - val preferredJavaThreadPriority = if (isMaxProfile) Thread.NORM_PRIORITY + 2 else Thread.NORM_PRIORITY + 1 context.log.info( - "Performance mode enabled: profile=$profile, threadPriority=$threadPriority, minFps=$minimumFrameRate, minRecordingFps=$minimumRecordingFrameRate, durationScale=$durationScale, rvCache=$recyclerViewCacheSize, maxRequests=$maxRequests/$maxRequestsPerHost, minCoreThreads=$minimumCoreThreads, prefetch=$prefetchItemCount, maxAnimMs=$maxAnimationDurationMs, maxScrollMs=$maxScrollDurationMs, preferredRefreshRate=$preferredRefreshRate, snapMapTransitionMs=$snapMapTransitionDurationMs, snapMapCameraMs=$snapMapCameraDurationMs, snapMapMoveMs=$snapMapMoveDurationMs, snapMapPrefetchZoomDelta=$snapMapPrefetchZoomDelta, javaThreadPriority=$preferredJavaThreadPriority", + "Performance mode enabled: profile=$profile, threadPriority=$threadPriority, minFps=$minimumFrameRate, durationScale=$durationScale, rvCache=$recyclerViewCacheSize, maxRequests=$maxRequests/$maxRequestsPerHost, minCoreThreads=$minimumCoreThreads, prefetch=$prefetchItemCount, maxAnimMs=$maxAnimationDurationMs, maxScrollMs=$maxScrollDurationMs, preferredRefreshRate=$preferredRefreshRate", "PerformanceMode" ) runCatching { ValueAnimator.setFrameDelay(0L) - context.log.info("Applied ValueAnimator frame delay override: 0ms", "PerformanceMode") } - fun firstHitLogger(name: String): (String) -> Unit { - val didLog = AtomicBoolean(false) - return { details -> - if (didLog.compareAndSet(false, true)) { - context.log.info("First hit: $name | $details", "PerformanceMode") - } - } - } - - val handlerThreadConstructorLog = firstHitLogger("HandlerThread.constructor") - val handlerThreadStartLog = firstHitLogger("HandlerThread.start") - val threadStartLog = firstHitLogger("Thread.start") - val executorLog = firstHitLogger("ThreadPoolExecutor.constructor") - val dispatcherLog = firstHitLogger("OkHttp.Dispatcher.constructor") - val animatorLog = firstHitLogger("ValueAnimator.getDurationScale") - val recyclerCtorLog = firstHitLogger("RecyclerView.constructor") - val recyclerAdapterLog = firstHitLogger("RecyclerView.setAdapter") - val recyclerLayoutManagerLog = firstHitLogger("RecyclerView.setLayoutManager") - val sqliteOpenLog = firstHitLogger("SQLiteDatabase.openDatabase") - val sqliteCreateLog = firstHitLogger("SQLiteDatabase.openOrCreateDatabase") - val mediaRecorderLog = firstHitLogger("MediaRecorder.setVideoFrameRate") - val overScrollerLog = firstHitLogger("OverScroller.startScroll") - val mapDialogLog = firstHitLogger("Dialog.show") - val mapViewLog = firstHitLogger("MapView.constructor") - val mapboxNetworkBlockLog = firstHitLogger("SnapMap.telemetryBlock") - val mapCameraAnimLog = firstHitLogger("SnapMap.mapAnimatorDuration") - val mapThreadLog = firstHitLogger("SnapMap.mapThread") - val mapRendererFpsLog = firstHitLogger("SnapMap.mapRendererFps") - val mapTransitionLog = firstHitLogger("SnapMap.transitionOptions") - val mapMoveLog = firstHitLogger("SnapMap.moveDuration") fun isPerformanceSensitiveThread(name: String?): Boolean { val normalizedName = name?.lowercase() ?: return false - return listOf("codec", "transcod", "feed", "story", "opera", "messag", "network", "db", "disk", "map", "mapbox", "snapmap", "viewport").any { + return listOf("camera", "preview", "codec", "render", "gl", "transcod", "lens", "feed", "story", "opera", "messag", "network", "db", "disk", "map", "mapbox", "snapmap", "viewport").any { normalizedName.contains(it) } } @@ -158,187 +73,11 @@ class PerformanceMode : Feature("Performance Mode") { return durationMs.coerceAtMost(maxDurationMs) } - val performanceCacheDir = File(context.androidContext.filesDir, "performance_mode_cache").apply { mkdirs() } - val chatFeedSnapshotFile = File(performanceCacheDir, "chat_feed_snapshot.json") - val lastChatFeedSnapshotWrite = AtomicLong(0L) - val chatFeedSnapshotServedThisProcess = AtomicBoolean(false) - - fun invalidateChatFeedSnapshot(reason: String) { - val deleted = runCatching { - if (!chatFeedSnapshotFile.exists()) return@runCatching false - chatFeedSnapshotFile.delete() - }.getOrDefault(false) - chatFeedSnapshotServedThisProcess.set(false) - if (deleted) { - context.log.info("Invalidated chat feed snapshot ($reason)", "PerformanceMode") - } - } - - Activity::class.java.hook("onResume", HookStage.AFTER) { - if (!isMaxProfile) return@hook - chatFeedSnapshotServedThisProcess.set(false) - } - - val windowStatePrefs = context.androidContext.getSharedPreferences("purrfectsnap_perf_message_windows", Context.MODE_PRIVATE) - val messageWindowStates = runCatching { - val raw = windowStatePrefs.getString("states", null).orEmpty() - if (raw.isBlank()) { - LinkedHashMap() - } else { - context.gson.fromJson>( - raw, - object : TypeToken>() {}.type - ) ?: LinkedHashMap() - } - }.getOrElse { LinkedHashMap() } - - fun persistMessageWindowStates() { - runCatching { - windowStatePrefs.edit().putString("states", context.gson.toJson(messageWindowStates)).apply() - }.onFailure { - context.log.error("Failed to persist message window states", it, "PerformanceMode") - } - } - - val snapshotQueryWhitespaceRegex = Regex("\\s+") - fun buildChatFeedSnapshotQueryKey(sql: String): String { - return sql.lowercase() - .replace(snapshotQueryWhitespaceRegex, " ") - .trim() - } - - fun isChatFeedQuery(sql: String): Boolean { - val normalized = buildChatFeedSnapshotQueryKey(sql) - if (!normalized.startsWith("select ")) return false - - val isFriendsFeedViewQuery = - normalized.startsWith("select * from friendsfeedview ") && - normalized.contains(" order by _id ") && - normalized.contains(" limit ") - - val isFeedEntryQuery = - normalized.startsWith("select * from feed_entry ") && - normalized.contains(" order by last_updated_timestamp desc ") && - normalized.contains(" limit ") - - return (isFriendsFeedViewQuery || isFeedEntryQuery) && - !normalized.contains("count(") && - !normalized.contains("select 0") && - !normalized.contains("where key = ?") && - !normalized.contains("where client_conversation_id = ?") - } - - fun cursorCell(cursor: Cursor, index: Int): SnapshotCell { - return when (cursor.getType(index)) { - Cursor.FIELD_TYPE_NULL -> SnapshotCell(Cursor.FIELD_TYPE_NULL) - Cursor.FIELD_TYPE_INTEGER -> SnapshotCell(Cursor.FIELD_TYPE_INTEGER, longValue = cursor.getLong(index)) - Cursor.FIELD_TYPE_FLOAT -> SnapshotCell(Cursor.FIELD_TYPE_FLOAT, doubleValue = cursor.getDouble(index)) - Cursor.FIELD_TYPE_STRING -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index)) - Cursor.FIELD_TYPE_BLOB -> SnapshotCell( - Cursor.FIELD_TYPE_BLOB, - blobValue = cursor.getBlob(index) - ?.takeIf { it.size <= CHAT_FEED_CACHE_MAX_BLOB_BYTES } - ?.let { Base64.encodeToString(it, Base64.NO_WRAP) } - ) - else -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index)) - } - } - - fun snapshotFromCursor(cursor: Cursor): CursorSnapshot? { - val originalPosition = cursor.position - val snapshot = runCatching { - val columns = cursor.columnNames.toList() - val rows = mutableListOf>() - if (cursor.moveToFirst()) { - var rowCount = 0 - do { - rows += columns.indices.map { index -> cursorCell(cursor, index) } - rowCount++ - } while (rowCount < CHAT_FEED_CACHE_MAX_ROWS && cursor.moveToNext()) - } - CursorSnapshot(columns, rows) - }.onFailure { - context.log.error("Failed to snapshot chat feed cursor", it, "PerformanceMode") - }.getOrNull() - - runCatching { cursor.moveToPosition(originalPosition) } - val restoredPosition = runCatching { cursor.position }.getOrNull() - if (restoredPosition != originalPosition) { - context.log.warn( - "Skipping chat feed snapshot write due non-restorable cursor position (from=$originalPosition to=${restoredPosition ?: "unknown"})", - "PerformanceMode" - ) - return null - } - return snapshot - } - - fun snapshotToMatrixCursor(snapshot: CursorSnapshot): MatrixCursor { - return MatrixCursor(snapshot.columns.toTypedArray(), snapshot.rows.size).also { matrixCursor -> - snapshot.rows.forEach { row -> - matrixCursor.addRow(row.map { cell -> - when (cell.type) { - Cursor.FIELD_TYPE_NULL -> null - Cursor.FIELD_TYPE_INTEGER -> cell.longValue - Cursor.FIELD_TYPE_FLOAT -> cell.doubleValue - Cursor.FIELD_TYPE_BLOB -> cell.blobValue?.let { Base64.decode(it, Base64.NO_WRAP) } - else -> cell.stringValue - } - }) - } - } - } - - fun readSnapshot(file: File, expectedQueryKey: String): CursorSnapshot? { - return runCatching { - if (!file.exists()) return null - val cache = context.gson.fromJson(file.readText(Charsets.UTF_8), ChatFeedSnapshotCache::class.java) ?: return null - if (cache.schemaVersion != CHAT_FEED_CACHE_SCHEMA_VERSION) { - runCatching { file.delete() } - return null - } - if (cache.queryKey != expectedQueryKey) { - runCatching { file.delete() } - return null - } - if (System.currentTimeMillis() - cache.createdAt > CHAT_FEED_CACHE_MAX_AGE_MS) { - runCatching { file.delete() } - return null - } - cache.snapshot - }.getOrElse { - runCatching { file.delete() } - null - } - } - - fun writeSnapshot(file: File, queryKey: String, snapshot: CursorSnapshot) { - runCatching { - file.writeText( - context.gson.toJson( - ChatFeedSnapshotCache( - schemaVersion = CHAT_FEED_CACHE_SCHEMA_VERSION, - queryKey = queryKey, - createdAt = System.currentTimeMillis(), - snapshot = snapshot, - ) - ), - Charsets.UTF_8 - ) - }.onFailure { - context.log.error("Failed to persist friend list snapshot", it, "PerformanceMode") - } - } - context.event.subscribe(NetworkApiRequestEvent::class) { event -> if (!isMaxProfile) return@subscribe val url = event.url - if (url.contains("ami/friends")) { - invalidateChatFeedSnapshot("friends-mutation-sync") - } if (url.contains("mapbox") && (url.contains("events.") || url.contains("telemetry"))) { event.canceled = true - mapboxNetworkBlockLog("url=$url") } } @@ -347,7 +86,6 @@ class PerformanceMode : Feature("Performance Mode") { val threadName = param.argNullable(0) if (!isPerformanceSensitiveThread(threadName)) return@hookConstructor param.setArg(1, threadPriority) - handlerThreadConstructorLog("name=$threadName priority=$threadPriority") } HandlerThread::class.java.hook("start", HookStage.AFTER) { param -> @@ -359,18 +97,13 @@ class PerformanceMode : Feature("Performance Mode") { Process.setThreadPriority(tid, threadPriority) } } - handlerThreadStartLog("name=${thread.name} tid=${thread.threadId} priority=$threadPriority") } Thread::class.java.hook("start", HookStage.AFTER) { param -> val thread = param.thisObject() if (!isPerformanceSensitiveThread(thread.name)) return@hook runCatching { - thread.priority = preferredJavaThreadPriority - } - threadStartLog("name=${thread.name} priority=${thread.priority}") - if ((thread.name ?: "").contains("map", ignoreCase = true) || (thread.name ?: "").contains("mapbox", ignoreCase = true)) { - mapThreadLog("name=${thread.name} priority=${thread.priority}") + thread.priority = if (isMaxProfile) Thread.MAX_PRIORITY else Thread.NORM_PRIORITY + 1 } } @@ -383,7 +116,6 @@ class PerformanceMode : Feature("Performance Mode") { } executor.allowCoreThreadTimeOut(false) executor.prestartAllCoreThreads() - executorLog("core=${executor.corePoolSize} max=${executor.maximumPoolSize} active=${executor.activeCount}") } } @@ -392,32 +124,62 @@ class PerformanceMode : Feature("Performance Mode") { runCatching { dispatcher.maxRequests = maxRequests dispatcher.maxRequestsPerHost = maxRequestsPerHost - dispatcherLog("maxRequests=${dispatcher.maxRequests} maxRequestsPerHost=${dispatcher.maxRequestsPerHost}") } } ValueAnimator::class.java.hook("getDurationScale", HookStage.AFTER) { param -> param.setResult(durationScale) - animatorLog("durationScale=$durationScale") + } + + ValueAnimator::class.java.hook("setDuration", HookStage.BEFORE) { param -> + val original = param.arg(0) + val updated = original.coerceAtMost(maxAnimationDurationMs) + if (updated != original) { + param.setArg(0, updated) + } + } + + ViewPropertyAnimator::class.java.hook("setDuration", HookStage.BEFORE) { param -> + val original = param.arg(0) + val updated = original.coerceAtMost(maxAnimationDurationMs) + if (updated != original) { + param.setArg(0, updated) + } + } + + Transition::class.java.hook("setDuration", HookStage.BEFORE) { param -> + val original = param.arg(0) + val updated = original.coerceAtMost(maxAnimationDurationMs) + if (updated != original) { + param.setArg(0, updated) + } + } + + Animation::class.java.hook("setDuration", HookStage.BEFORE) { param -> + val original = param.arg(0) + val updated = original.coerceAtMost(maxAnimationDurationMs) + if (updated != original) { + param.setArg(0, updated) + } } RecyclerView::class.java.hookConstructor(HookStage.AFTER) { param -> val recyclerView = param.thisObject() recyclerView.setItemViewCacheSize(recyclerViewCacheSize) recyclerView.overScrollMode = View.OVER_SCROLL_NEVER + recyclerView.recycledViewPool.setMaxRecycledViews(0, 20) if (isMaxProfile) { recyclerView.itemAnimator = null } - recyclerCtorLog("cache=$recyclerViewCacheSize max=$isMaxProfile class=${recyclerView::class.java.name}") } RecyclerView::class.java.hook("setAdapter", HookStage.AFTER) { param -> val recyclerView = param.thisObject() recyclerView.setItemViewCacheSize(recyclerViewCacheSize) + recyclerView.recycledViewPool.setMaxRecycledViews(0, 20) if (isMaxProfile) { recyclerView.itemAnimator = null } - recyclerAdapterLog("cache=$recyclerViewCacheSize adapter=${param.argNullable(0)?.javaClass?.name}") } RecyclerView::class.java.hook("setLayoutManager", HookStage.AFTER) { param -> @@ -426,14 +188,13 @@ class PerformanceMode : Feature("Performance Mode") { when (layoutManager) { is LinearLayoutManager -> { layoutManager.isItemPrefetchEnabled = true - layoutManager.initialPrefetchItemCount = prefetchItemCount + layoutManager.initialPrefetchItemCount = prefetchItemCount.coerceAtLeast(12) } is StaggeredGridLayoutManager -> { layoutManager.isItemPrefetchEnabled = true layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS } } - recyclerLayoutManagerLog("layoutManager=${layoutManager?.javaClass?.name} prefetch=$prefetchItemCount") } fun SQLiteDatabase.applyPerformancePragmas() { @@ -446,28 +207,21 @@ class PerformanceMode : Feature("Performance Mode") { } SQLiteDatabase::class.java.hook("openDatabase", HookStage.AFTER) { param -> - (param.getResult() as? SQLiteDatabase)?.also { - it.applyPerformancePragmas() - sqliteOpenLog("path=${param.argNullable(0)}") - } + (param.getResult() as? SQLiteDatabase)?.applyPerformancePragmas() } SQLiteDatabase::class.java.hook("openOrCreateDatabase", HookStage.AFTER) { param -> - (param.getResult() as? SQLiteDatabase)?.also { - it.applyPerformancePragmas() - sqliteCreateLog("path=${param.argNullable(0)}") - } + (param.getResult() as? SQLiteDatabase)?.applyPerformancePragmas() } MediaRecorder::class.java.hook("setVideoFrameRate", HookStage.BEFORE) { param -> val currentRate = param.arg(0) val applied = currentRate - .coerceAtLeast(minimumRecordingFrameRate) + .coerceAtLeast(if (isMaxProfile) 30 else 24) .coerceAtMost(if (isMaxProfile) 60 else 45) if (applied != currentRate) { param.setArg(0, applied) } - mediaRecorderLog("requested=$currentRate applied=${param.arg(0)}") } OverScroller::class.java.hook("startScroll", HookStage.BEFORE) { param -> @@ -477,7 +231,6 @@ class PerformanceMode : Feature("Performance Mode") { if (updated != original) { param.setArg(4, updated) } - overScrollerLog("requested=$original applied=${param.arg(4)}") } } @@ -490,46 +243,6 @@ class PerformanceMode : Feature("Performance Mode") { } } - fun applyActivityPerformanceTuning(activity: Activity) { - runCatching { - activity.window.setWindowAnimations(0) - } - } - - onNextActivityCreate { - applyActivityPerformanceTuning(it) - } - - Dialog::class.java.hook("show", HookStage.AFTER) { param -> - val dialog = param.nullableThisObject() as? Dialog ?: return@hook - val window = dialog.window ?: return@hook - runCatching { - window.setWindowAnimations(0) - if (dialog::class.java.name.contains("map", ignoreCase = true) || dialog::class.java.name.contains("snap", ignoreCase = true)) { - mapDialogLog("class=${dialog::class.java.name}") - } - } - } - - runCatching { - findClass("com.mapbox.mapboxsdk.maps.MapView").hookConstructor(HookStage.AFTER) { param -> - val mapView = param.nullableThisObject() as? View ?: return@hookConstructor - mapView.overScrollMode = View.OVER_SCROLL_NEVER - mapViewLog("class=${mapView::class.java.name}") - } - } - - runCatching { - findClass("com.mapbox.mapboxsdk.maps.renderer.MapRenderer").hook("setMaximumFps", HookStage.BEFORE) { param -> - val requested = param.arg(0) - val applied = requested.coerceAtLeast(120) - if (applied != requested) { - param.setArg(0, applied) - } - mapRendererFpsLog("requested=$requested applied=${param.arg(0)}") - } - } - runCatching { val nativeMapViewClass = findClass("com.mapbox.mapboxsdk.maps.NativeMapView") val transitionOptionsClass = findClass("com.mapbox.mapboxsdk.style.layers.TransitionOptions") @@ -546,9 +259,9 @@ class PerformanceMode : Feature("Performance Mode") { } val nativeCancelTransitions = findNativeMapMethod("nativeCancelTransitions") { it.parameterCount == 0 } - val nativeSetPrefetchTiles = findNativeMapMethod("nativeSetPrefetchTiles") { it.parameterCount == 1 && it.parameterTypes[0] == Boolean::class.javaPrimitiveType } + val nativeSetPrefetchTiles = findNativeMapMethod("nativeSetPrefetchTiles") { it.parameterCount == 1 && it.parameterTypes[0] == Boolean::class.javaPrimitiveType } val nativeSetPrefetchZoomDelta = findNativeMapMethod("nativeSetPrefetchZoomDelta") { it.parameterCount == 1 && it.parameterTypes[0] == Int::class.javaPrimitiveType } - val nativeSetTransitionDelay = findNativeMapMethod("nativeSetTransitionDelay") { it.parameterCount == 1 && it.parameterTypes[0] == Long::class.javaPrimitiveType } + val nativeSetTransitionDelay = findNativeMapMethod("nativeSetTransitionDelay") { it.parameterCount == 1 && it.parameterTypes[0] == Long::class.javaPrimitiveType } val nativeSetTransitionDuration = findNativeMapMethod("nativeSetTransitionDuration") { it.parameterCount == 1 && it.parameterTypes[0] == Long::class.javaPrimitiveType } val nativeSetTransitionOptions = findNativeMapMethod("nativeSetTransitionOptions") { it.parameterCount == 1 && it.parameterTypes[0].name == transitionOptionsClass.name } @@ -556,15 +269,14 @@ class PerformanceMode : Feature("Performance Mode") { val nativeMapView = param.thisObject() runCatching { nativeSetPrefetchTiles?.invoke(nativeMapView, true) - nativeSetPrefetchZoomDelta?.invoke(nativeMapView, snapMapPrefetchZoomDelta) + nativeSetPrefetchZoomDelta?.invoke(nativeMapView, 6) nativeSetTransitionDelay?.invoke(nativeMapView, 0L) - nativeSetTransitionDuration?.invoke(nativeMapView, snapMapTransitionDurationMs) + nativeSetTransitionDuration?.invoke(nativeMapView, 0L) nativeSetTransitionOptions?.invoke( nativeMapView, - transitionOptionsCtor.newInstance(snapMapTransitionDurationMs, 0L, false) + transitionOptionsCtor.newInstance(0L, 0L, false) ) nativeCancelTransitions?.invoke(nativeMapView) - mapTransitionLog("transitionMs=$snapMapTransitionDurationMs prefetchZoomDelta=$snapMapPrefetchZoomDelta placementTransitions=false") } } @@ -574,12 +286,11 @@ class PerformanceMode : Feature("Performance Mode") { method.parameterTypes.last() == Long::class.javaPrimitiveType }?.hook(HookStage.BEFORE) { param -> val original = param.arg(5) - val applied = clampPositiveDuration(original, snapMapCameraDurationMs) + val applied = original.coerceAtMost(16L) if (applied != original) { param.setArg(5, applied) } runCatching { nativeCancelTransitions?.invoke(param.thisObject()) } - mapCameraAnimLog("requested=$original applied=${param.arg(5)}") } nativeMapViewClass.findRestrictedMethod { method -> @@ -590,125 +301,57 @@ class PerformanceMode : Feature("Performance Mode") { method.parameterTypes[2] == Long::class.javaPrimitiveType }?.hook(HookStage.BEFORE) { param -> val original = param.arg(2) - val applied = clampPositiveDuration(original, snapMapMoveDurationMs) + val applied = original.coerceAtMost(8L) if (applied != original) { param.setArg(2, applied) } runCatching { nativeCancelTransitions?.invoke(param.thisObject()) } - mapMoveLog("requested=$original applied=${param.arg(2)}") } }.onFailure { context.log.error("Failed to install Snap Map transition hooks", it, "PerformanceMode") } - runCatching { - findClass("com.snapchat.client.messaging.MessageWindowManager\$CppProxy").hook("initWindow", HookStage.BEFORE) { param -> - if (!isMaxProfile) return@hook - - val conversationId = runCatching { - SnapUUID(param.arg(0)).toString() - }.getOrNull()?.takeIf { it.isNotBlank() } ?: return@hook - - val initParams = param.arg(1) - val conversationType = context.database.getConversationType(conversationId) ?: return@hook - val isGroup = conversationType == 1 - val savedState = synchronized(messageWindowStates) { - messageWindowStates[conversationId] - ?.takeIf { System.currentTimeMillis() - it.updatedAt <= MESSAGE_WINDOW_STATE_MAX_AGE_MS } - } - - val enumConstants = initParams.getObjectField("mStartingType")?.javaClass?.enumConstants ?: return@hook - if (savedState != null) { - val restoredMaxSize = if (savedState.isGroup) { - savedState.currentSize.coerceAtLeast(220).coerceAtMost(520) - } else { - savedState.currentSize.coerceAtLeast(140).coerceAtMost(320) - } - val restoredForward = (savedState.currentSize + if (savedState.isGroup) 24 else 16).coerceAtMost(restoredMaxSize) - val restoredBack = if (savedState.isGroup) 180 else 120 - initParams.setObjectField("mStartingType", enumConstants.firstOrNull { it.toString() == "MESSAGE" } ?: return@hook) - initParams.setObjectField("mStartingOrderKey", savedState.oldestOrderKey ?: savedState.newestOrderKey) - initParams.setObjectField("mMaxSize", restoredMaxSize) - initParams.setObjectField("mNumMessagesForward", restoredForward) - initParams.setObjectField("mNumMessagesBack", restoredBack) - - val warmupAmount = if (savedState.isGroup) REOPEN_WARMUP_GROUP_MESSAGES else REOPEN_WARMUP_DM_MESSAGES - val oldestKey = savedState.oldestOrderKey - if (oldestKey != null) { - context.feature(Messaging::class).conversationManager?.fetchConversationWithMessagesPaginated( - conversationId = conversationId, - lastMessageId = oldestKey, - amount = warmupAmount, - onSuccess = {}, - onError = {} - ) - } + fun applyActivityPerformanceTuning(activity: Activity) { + runCatching { + activity.window.decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null) + val display = activity.display + val targetRefreshRate = display?.supportedModes?.maxByOrNull { it.refreshRate }?.refreshRate + ?.coerceAtLeast(preferredRefreshRate) ?: preferredRefreshRate + activity.window.attributes = activity.window.attributes.apply { + this.preferredRefreshRate = targetRefreshRate } } - }.onFailure { - context.log.error("Failed to install saved message window restore hooks", it, "PerformanceMode") - } - - context.mappings.useMapper(CallbackMapper::class) { - callbacks.getClass("MessageWindowManagerDelegate")?.hook("onWindowUpdated", HookStage.AFTER) { param -> - if (!isMaxProfile) return@hook - - val conversationId = runCatching { SnapUUID(param.arg(0)).toString() }.getOrNull() ?: return@hook - val update = param.arg(2) - val pagination = update.getObjectField("mPagination") ?: return@hook - val currentSize = pagination.getObjectField("mCurrentSize") as? Int ?: return@hook - val oldestOrderKey = pagination.getObjectField("mOldestOrderKey") as? Long - val newestOrderKey = pagination.getObjectField("mNewestOrderKey") as? Long - val conversationType = context.database.getConversationType(conversationId) ?: 0 - val isGroup = conversationType == 1 - - synchronized(messageWindowStates) { - messageWindowStates[conversationId] = MessageWindowState( - conversationId = conversationId, - currentSize = currentSize.coerceAtMost(if (isGroup) 420 else 260), - oldestOrderKey = oldestOrderKey, - newestOrderKey = newestOrderKey, - updatedAt = System.currentTimeMillis(), - isGroup = isGroup - ) - while (messageWindowStates.size > MESSAGE_WINDOW_STATE_MAX_CONVERSATIONS) { - val eldestKey = messageWindowStates.entries.minByOrNull { it.value.updatedAt }?.key ?: break - messageWindowStates.remove(eldestKey) - } - persistMessageWindowStates() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isMaxProfile) { + runCatching { + activity.window.setSustainedPerformanceMode(true) } } } - runCatching { - findClass("io.requery.android.database.sqlite.SQLiteDatabase").hook("rawQueryWithFactory", HookStage.BEFORE) { param -> - if (!isMaxProfile) return@hook - val sql = param.argNullable(1) ?: return@hook - if (!isChatFeedQuery(sql)) return@hook - if (chatFeedSnapshotServedThisProcess.get()) return@hook - val queryKey = buildChatFeedSnapshotQueryKey(sql) - readSnapshot(chatFeedSnapshotFile, queryKey)?.let { snapshot -> - param.setResult(snapshotToMatrixCursor(snapshot)) - chatFeedSnapshotServedThisProcess.set(true) - } - } - - findClass("io.requery.android.database.sqlite.SQLiteDatabase").hook("rawQueryWithFactory", HookStage.AFTER) { param -> - if (!isMaxProfile) return@hook - val sql = param.argNullable(1) ?: return@hook - if (!isChatFeedQuery(sql)) return@hook - val cursor = param.getResult() as? Cursor ?: return@hook - val now = System.currentTimeMillis() - if (now - lastChatFeedSnapshotWrite.get() < CHAT_FEED_CACHE_MIN_REFRESH_INTERVAL_MS) return@hook - val queryKey = buildChatFeedSnapshotQueryKey(sql) - val snapshot = snapshotFromCursor(cursor) ?: return@hook - if (snapshot.rows.isEmpty()) return@hook - writeSnapshot(chatFeedSnapshotFile, queryKey, snapshot) - lastChatFeedSnapshotWrite.set(now) - } - }.onFailure { - context.log.error("Failed to install chat feed cache hooks", it, "PerformanceMode") + onNextActivityCreate { + applyActivityPerformanceTuning(it) } + Dialog::class.java.hook("show", HookStage.AFTER) { param -> + val dialog = param.nullableThisObject() as? Dialog ?: return@hook + val window = dialog.window ?: return@hook + runCatching { + window.setWindowAnimations(0) + window.decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null) + window.attributes = window.attributes.apply { + flags = flags or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED + } + } + } + + TextureView::class.java.hookConstructor(HookStage.AFTER) { param -> + val textureView = param.thisObject() + runCatching { + // Universal Guard: Only accelerate views owned by Snapchat. + // This prevents crashes in native hardware providers across all devices. + if (textureView.context.packageName != context.androidContext.packageName) return@runCatching + textureView.setLayerType(View.LAYER_TYPE_HARDWARE, null) + } + } } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/FakeFollowersCount.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/FakeFollowersCount.kt new file mode 100644 index 00000000..ed050060 --- /dev/null +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/FakeFollowersCount.kt @@ -0,0 +1,130 @@ +package me.eternal.purrfectsnap.core.features.impl.ui + +import android.widget.TextView +import me.eternal.purrfectsnap.core.features.Feature +import me.eternal.purrfectsnap.core.util.hook.HookStage +import me.eternal.purrfectsnap.core.util.hook.hook +import java.math.BigDecimal +import java.math.BigInteger +import java.math.MathContext +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Locale + +class FakeFollowersCount : Feature("Fake Followers Count") { + + override fun init() { + if (context.config.userInterface.spoofFollowersCount.globalState != true) return + val raw = context.config.userInterface.spoofFollowersCount.customFollowersCount.getNullable()?.trim()?.takeIf { it.isNotBlank() } + ?: return + val digits = raw.replace(Regex("[^0-9]"), "") + if (digits.isEmpty()) return + val followerValue = try { + BigInteger(digits) + } catch (_: NumberFormatException) { + return + } + + onNextActivityCreate { + TextView::class.java.hook("setText", HookStage.BEFORE) { param -> + val textView = param.thisObject() as? TextView ?: return@hook + if (textView.javaClass.name !in COMPOSER_SNAP_TEXT_VIEW) return@hook + val arg0 = param.argNullable(0) ?: return@hook + if (arg0 !is CharSequence) return@hook + val text = arg0.toString() + if (!FOLLOWERS_LINE.containsMatchIn(text)) return@hook + val display = formatFollowersDisplay(followerValue) + val replaced = FOLLOWERS_LINE.replace(text) { mr -> + "$display ${mr.groupValues[2]}" + } + param.setArg(0, replaced) + } + } + } + + private fun formatFollowersDisplay(n: BigInteger): String { + val v = n.max(BigInteger.ZERO) + if (v < TEN_THOUSAND) { + return NumberFormat.getIntegerInstance(Locale.US).format(v.toLong()) + } + val (scaled, suffix) = scaleToTier(v) + return formatMantissaMaxFourDigits(scaled) + suffix + } + + private fun scaleToTier(v: BigInteger): Pair { + var i = SCALE_TIERS.indexOfLast { v >= it.floor }.coerceAtLeast(0) + var floor = SCALE_TIERS[i].floor + var suffix = SCALE_TIERS[i].suffix + var scaled = BigDecimal(v, MATH_CTX).divide(BigDecimal(floor, MATH_CTX), MATH_CTX) + while (scaled >= THOUSAND_BD && i + 1 < SCALE_TIERS.size) { + i++ + floor = SCALE_TIERS[i].floor + suffix = SCALE_TIERS[i].suffix + scaled = BigDecimal(v, MATH_CTX).divide(BigDecimal(floor, MATH_CTX), MATH_CTX) + } + return Pair(scaled, suffix) + } + + private fun formatMantissaMaxFourDigits(scaled: BigDecimal): String { + val x = scaled.abs().setScale(12, RoundingMode.HALF_UP) + if (x.compareTo(BigDecimal.ZERO) == 0) return "0" + if (x >= HUNDRED) { + val i = x.setScale(0, RoundingMode.HALF_UP) + var s = i.toPlainString() + if (digitCount(s) > 4) { + s = x.round(MathContext(4, RoundingMode.HALF_UP)).setScale(0, RoundingMode.HALF_UP).toPlainString() + } + return s + } + if (x >= TEN) { + var s = stripFrac(x.setScale(1, RoundingMode.HALF_UP)) + if (digitCount(s) > 4) { + s = x.setScale(0, RoundingMode.HALF_UP).toPlainString() + } + return s + } + var s = stripFrac(x.setScale(2, RoundingMode.HALF_UP)) + if (digitCount(s) > 4) { + s = stripFrac(x.setScale(1, RoundingMode.HALF_UP)) + } + if (digitCount(s) > 4) { + s = x.setScale(0, RoundingMode.HALF_UP).toPlainString() + } + return s + } + + private fun digitCount(s: String) = s.count { it.isDigit() } + + private fun stripFrac(d: BigDecimal): String { + var s = d.stripTrailingZeros().toPlainString() + if ('.' in s) { + s = s.trimEnd('0').trimEnd('.') + } + return s + } + + private data class ScaleTier(val floor: BigInteger, val suffix: String) + + companion object { + private val FOLLOWERS_LINE = Regex("^([\\d,]+)\\s+(Followers)\\b", RegexOption.IGNORE_CASE) + private val COMPOSER_SNAP_TEXT_VIEW = setOf( + "com.snap.valdi.views.ComposerSnapTextView", + "com.snap.composer.views.ComposerSnapTextView", + ) + private val MATH_CTX = MathContext(24, RoundingMode.HALF_UP) + private val TEN_THOUSAND = BigInteger("10000") + private val THOUSAND_BD = BigDecimal("1000") + private val HUNDRED = BigDecimal("100") + private val TEN = BigDecimal("10") + private val SCALE_TIERS = listOf( + ScaleTier(BigInteger("1000"), "K"), + ScaleTier(BigInteger("1000000"), "M"), + ScaleTier(BigInteger("1000000000"), "B"), + ScaleTier(BigInteger("1000000000000"), "T"), + ScaleTier(BigInteger("1000000000000000"), "Q"), + ScaleTier(BigInteger("1000000000000000000"), "E"), + ScaleTier(BigInteger("1000000000000000000000"), "Z"), + ScaleTier(BigInteger("1000000000000000000000000"), "Y"), + ) + } +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/FriendFeedMessagePreview.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/FriendFeedMessagePreview.kt index 625c35fa..1fcb9fbb 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/FriendFeedMessagePreview.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/FriendFeedMessagePreview.kt @@ -9,6 +9,8 @@ import android.text.TextPaint import android.view.View import android.view.ViewGroup import android.graphics.Typeface +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch @@ -78,9 +80,10 @@ class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview") { val ffSdlPrimaryTextStartMargin = 6 * density val feedEntryHeight = ffSdlAvatarSize + ffSdlAvatarMargin * 2 + (4 * density).toInt() - val separatorHeight = (density * 2).toInt() + val safetyGap = (6 * density).toInt() val textPaint = TextPaint().apply { textSize = secondaryTextSize + isAntiAlias = true } context.event.subscribe(BuildMessageEvent::class) { param -> @@ -105,14 +108,15 @@ class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview") { } fetchMessages(conversationId) { - var maxTextHeight = 0 - val previewContainerHeight = messageCache[conversationId]?.sumOf { msg -> - val rect = Rect() - textPaint.getTextBounds(msg, 0, msg.length, rect) - rect.height().also { - if (it > maxTextHeight) maxTextHeight = it - }.plus(separatorHeight) - } ?: run { + val universalTextSize = 12 * density + val fontMetrics = textPaint.apply { textSize = universalTextSize }.fontMetrics + val lineHeight = (fontMetrics.descent - fontMetrics.ascent).toInt() + val spacing = (4 * density).toInt() + + val messages = messageCache[conversationId] + val previewContainerHeight = if (messages.isNullOrEmpty()) 0 else (messages.size * (lineHeight + spacing)) + + if (previewContainerHeight == 0) { ffItem.layoutParams = ffItem.layoutParams.apply { height = ViewGroup.LayoutParams.MATCH_PARENT } @@ -120,22 +124,23 @@ class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview") { } ffItem.layoutParams = ffItem.layoutParams.apply { - height = feedEntryHeight + previewContainerHeight + separatorHeight + height = feedEntryHeight + (safetyGap).toInt() + previewContainerHeight } cachedLayouts[conversationId] = frameLayout frameLayout.addForegroundDrawable("ffItem", ShapeDrawable(object: Shape() { override fun draw(canvas: Canvas, paint: Paint) { - val offsetY = canvas.height.toFloat() - previewContainerHeight - paint.textSize = secondaryTextSize - paint.color = context.userInterface.colorPrimary + val startY = feedEntryHeight.toFloat() - (9 * density) + paint.textSize = universalTextSize + paint.color = Color(context.userInterface.colorPrimary).copy(alpha = 0.85f).toArgb() paint.typeface = Typeface.DEFAULT + paint.isAntiAlias = true - messageCache[conversationId]?.forEachIndexed { index, messageString -> + messages?.forEachIndexed { index, messageString -> canvas.drawText(messageString, - feedEntryHeight + ffSdlPrimaryTextStartMargin, - offsetY + index * maxTextHeight, + ffSdlAvatarSize + ffSdlAvatarMargin + (ffSdlPrimaryTextStartMargin * 3), + startY + (index + 1) * lineHeight + (index * spacing), paint ) } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/HideFriendFeedEntry.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/HideFriendFeedEntry.kt index b9f47a7c..8ad88ce6 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/HideFriendFeedEntry.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/HideFriendFeedEntry.kt @@ -1,17 +1,31 @@ package me.eternal.purrfectsnap.core.features.impl.ui +import android.view.View import me.eternal.purrfectsnap.common.data.MessagingRuleType import me.eternal.purrfectsnap.common.data.RuleState - +import me.eternal.purrfectsnap.core.event.events.impl.BindViewEvent import me.eternal.purrfectsnap.core.features.MessagingRuleFeature +import me.eternal.purrfectsnap.core.ui.hideViewCompletely import me.eternal.purrfectsnap.core.util.dataBuilder import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.ktx.getObjectField import me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID import me.eternal.purrfectsnap.mapper.impl.CallbackMapper +import java.util.ArrayList +import java.util.concurrent.ConcurrentHashMap class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType = MessagingRuleType.HIDE_FRIEND_FEED) { + @Volatile + private var cachedRuleIds: Set = emptySet() + + @Volatile + private var cachedRuleIdsAt = 0L + + private val conversationTargetsCache = ConcurrentHashMap>() + private val hideDecisionCache = ConcurrentHashMap() + private var lastRuleIdsHash = 0 + private fun createDeletedFeedEntry(conversationIdInstance: Any) = findClass("com.snapchat.client.messaging.DeletedFeedEntry").dataBuilder { from("mFeedEntryIdentifier") { set("mConversationId", conversationIdInstance) @@ -19,10 +33,59 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType set("mReason", "CLEAR_CONVERSATION") } - private fun filterFriendFeed(entries: ArrayList, deletedEntries: ArrayList? = null) { + private fun getRuleIdsSnapshot(): Set { + val now = System.currentTimeMillis() + if (now - cachedRuleIdsAt <= 1_000L) return cachedRuleIds + + return context.bridgeClient.getRuleIds(ruleType).toSet().also { + cachedRuleIds = it + cachedRuleIdsAt = now + } + } + + private fun resolveRuleTargets(conversationId: String): Set { + return conversationTargetsCache.getOrPut(conversationId) { + val targets = linkedSetOf(conversationId) + context.database.getDMOtherParticipant(conversationId)?.let { targets.add(it) } + context.database.getFeedEntryByConversationId(conversationId)?.let { entry -> + entry.friendUserId?.let { targets.add(it) } + entry.participants?.forEach { targets.add(it) } + } + targets + } + } + + private fun shouldHideConversation( + conversationId: String, + ruleIds: Set, + ruleState: RuleState? + ): Boolean { + if (ruleState == null) return false + + // Industrial Cache Gating: Clear decisions if the master rule list changed + val currentHash = ruleIds.hashCode() + if (currentHash != lastRuleIdsHash) { + hideDecisionCache.clear() + lastRuleIdsHash = currentHash + } + + return hideDecisionCache.getOrPut(conversationId) { + val isExplicitRuleMatch = resolveRuleTargets(conversationId).any { it in ruleIds } + if (ruleState == RuleState.BLACKLIST) !isExplicitRuleMatch else isExplicitRuleMatch + } + } + + private fun filterFriendFeed( + entries: ArrayList, + ruleIds: Set, + ruleState: RuleState?, + deletedEntries: ArrayList? = null + ) { + if (ruleState == null || entries.isEmpty()) return entries.removeIf { feedEntry -> val conversationIdInstance = feedEntry.getObjectField("mConversationId") ?: return@removeIf false - if (canUseRule(SnapUUID(conversationIdInstance).toString())) { + val conversationId = SnapUUID(conversationIdInstance).toString() + if (shouldHideConversation(conversationId, ruleIds, ruleState)) { deletedEntries?.add(createDeletedFeedEntry(conversationIdInstance)!!) true } else { @@ -31,6 +94,20 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType } } + private fun hideBoundChatFeedRow(view: View) { + var current: View? = view + repeat(4) { + val parent = current?.parent as? View + // Safety: Never hide the actual list container + if (parent?.javaClass?.name?.contains("RecyclerView") == true) { + current?.hideViewCompletely() + return + } + current?.hideViewCompletely() + current = parent + } + } + private fun hookCallbackMethod( hookedCallbacks: MutableSet, callbackClassName: String, @@ -51,6 +128,14 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType override fun init() { if (!context.config.userInterface.hideFriendFeedEntry.get()) return + context.event.subscribe(BindViewEvent::class) { event -> + event.friendFeedItem { conversationId -> + if (shouldHideConversation(conversationId, getRuleIdsSnapshot(), getRuleState())) { + hideBoundChatFeedRow(event.view) + } + } + } + context.mappings.useMapper(CallbackMapper::class) { classLoader = context.androidContext.classLoader val hasFetchAndSyncCallback = callbacks.getAsMap()?.entries?.any { @@ -69,13 +154,13 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType when { callbackName.startsWith("FetchAndSyncFeed") && callbackName.endsWith("Callback") -> { hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFetchAndSyncFeedComplete") { param -> - val deletedConversations: ArrayList = param.arg(2) - filterFriendFeed(param.arg(0), deletedConversations) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + val deletedConversations = param.argNullable>(2) + val ruleIds = getRuleIdsSnapshot() + val ruleState = getRuleState() + filterFriendFeed(entries, ruleIds, ruleState, deletedConversations) - if (deletedConversations.any { - val uuid = SnapUUID(it.getObjectField("mFeedEntryIdentifier")?.getObjectField("mConversationId")).toString() - context.database.getFeedEntryByConversationId(uuid) != null - }) { + if (deletedConversations?.isNotEmpty() == true) { param.setArg(4, true) } } @@ -83,34 +168,40 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType callbackName.contains("SyncFeed") && callbackName.endsWith("Callback") -> { hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onSyncFeedComplete") { param -> - filterFriendFeed(param.arg(0), param.argNullable(2)) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + filterFriendFeed(entries, getRuleIdsSnapshot(), getRuleState(), param.argNullable(2)) } } callbackName == "FetchFeedCallback" || callbackName.contains("FetchFeedCallback") -> { hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFetchFeedComplete") { param -> - filterFriendFeed(param.arg(0)) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + filterFriendFeed(entries, getRuleIdsSnapshot(), getRuleState()) } } callbackName == "FetchFeedEntriesCallback" || callbackName.contains("FetchFeedEntriesCallback") -> { hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFetchFeedEntriesComplete") { param -> - filterFriendFeed(param.arg(0)) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + filterFriendFeed(entries, getRuleIdsSnapshot(), getRuleState()) } } callbackName == "QueryFeedCallback" || callbackName.contains("QueryFeedCallback") -> { hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onQueryFeedComplete") { param -> - filterFriendFeed(param.arg(0)) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + filterFriendFeed(entries, getRuleIdsSnapshot(), getRuleState()) } } callbackName == "FeedManagerDelegate" -> { hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFeedEntriesUpdated") { param -> - filterFriendFeed(param.arg(0)) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + filterFriendFeed(entries, getRuleIdsSnapshot(), getRuleState()) } hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onInternalSyncFeed") { param -> - filterFriendFeed(param.arg(0)) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + filterFriendFeed(entries, getRuleIdsSnapshot(), getRuleState()) } } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/MessageIndicators.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/MessageIndicators.kt index f3bb340b..e5521879 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/MessageIndicators.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/MessageIndicators.kt @@ -8,11 +8,18 @@ import androidx.compose.foundation.layout.* 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.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -25,6 +32,47 @@ import me.eternal.purrfectsnap.core.features.Feature import me.eternal.purrfectsnap.core.ui.AppleLogo import kotlin.random.Random +@Composable +private fun GradientIcon( + imageVector: ImageVector, + brush: Brush, + size: androidx.compose.ui.unit.Dp = 16.dp +) { + Image( + imageVector = imageVector, + contentDescription = null, + colorFilter = ColorFilter.tint(Color.White), + modifier = Modifier + .size(size) + .graphicsLayer(alpha = 0.99f) + .drawWithContent { + drawContent() + drawRect(brush = brush, blendMode = BlendMode.SrcAtop) + } + ) +} + +@Composable +private fun GradientText( + text: String, + brush: Brush, + fontWeight: FontWeight, + fontSize: androidx.compose.ui.unit.TextUnit +) { + Text( + text = text, + color = Color.White, + fontWeight = fontWeight, + fontSize = fontSize, + modifier = Modifier + .graphicsLayer(alpha = 0.99f) + .drawWithContent { + drawContent() + drawRect(brush = brush, blendMode = BlendMode.SrcAtop) + } + ) +} + class MessageIndicators : Feature("Message Indicators") { override fun init() { val messageIndicatorsConfig = context.config.userInterface.messageIndicators.getNullable() ?: return @@ -35,28 +83,43 @@ class MessageIndicators : Feature("Message Indicators") { val appleLogo = AppleLogo context.event.subscribe(BindViewEvent::class) { event -> - event.chatMessage { _, _ -> + event.chatMessage { conversationId, _ -> val view = event.view as? ViewGroup ?: return@subscribe view.findViewWithTag(messageInfoTag)?.let { view.removeView(it) } val message = event.databaseMessage ?: return@chatMessage if (message.contentType != ContentType.SNAP.id && message.contentType != ContentType.EXTERNAL_MEDIA.id) return@chatMessage + if (message.senderId == context.database.myUserId && messageIndicatorsConfig.contains("skip_own_indicators")) return@chatMessage val reader = ProtoReader(message.messageContent ?: return@chatMessage) + val isGroupConversation = (context.database.getConversationParticipants(conversationId)?.size ?: 0) > 2 + if (isGroupConversation && messageIndicatorsConfig.contains("disable_indicators_in_groups")) return@chatMessage createComposeView(event.view.context) { + val lockBrush = Brush.linearGradient(listOf(Color(0xFF4CD471), Color(0xFF00B8D9))) + val locationBrush = Brush.linearGradient(listOf(Color(0xFFFF4B5C), Color(0xFFFFB74D))) + val androidBrush = Brush.linearGradient(listOf(Color(0xFF3DDC84), Color(0xFFA5E635))) + val appleBrush = Brush.linearGradient(listOf(Color(0xFFFFFFFF), Color(0xFF94A3B8))) + val webBrush = Brush.linearGradient(listOf(Color(0xFF4FC3F7), Color(0xFF00E5FF))) + val directorBrush = Brush.linearGradient(listOf(Color(0xFFFFB74D), Color(0xFFFF5E3A))) + val ovfBrush = Brush.linearGradient(listOf(Color(0xFFFF5CA8), Color(0xFFA64CFF))) + val memoriesBrush = Brush.linearGradient(listOf(Color(0xFFFFE066), Color(0xFFFFB300))) + Box( modifier = Modifier .fillMaxWidth() .height(50.dp) - .padding(top = 4.dp, end = 1.dp), + .padding(top = 6.dp, end = 6.dp), contentAlignment = Alignment.TopEnd ) { val hasEncryption by rememberAsyncMutableState(defaultValue = false) { - // Strictly detect Private Fidelius Wrap (1-on-1 private snaps) - reader.containsPath(4, 4, 1, 1) || - reader.containsPath(4, 4, 1, 1, 1) || - reader.getByteArray(4, 3, 3) != null || - reader.containsPath(3, 99, 3) + if (reader.containsPath(4, 4, 1, 1) + || reader.containsPath(4, 4, 1, 1, 1) + || reader.getByteArray(4, 3, 3) != null + || reader.containsPath(3, 99, 3)) { + return@rememberAsyncMutableState true + } + if (reader.containsPath(4, 5, 1, 3, 1)) return@rememberAsyncMutableState true + reader.getVarInt(4, 5, 1, 3, 2, 9) in setOf(1L, 3L) } val sentFromIosDevice by rememberAsyncMutableState(defaultValue = false) { if (reader.containsPath(4, 4, 3)) !reader.containsPath(4, 4, 3, 3, 17) else reader.getVarInt(4, 4, 11, 17, 7) != null @@ -75,52 +138,41 @@ class MessageIndicators : Feature("Message Indicators") { (it.getVarInt(1) to it.getVarInt(2)) == (0L to 0L) } == true || reader.getByteArray(4, 4, 11, 13, 4, 1, 2, 12, 27, 1) != null } + val sentFromMemories by rememberAsyncMutableState(defaultValue = false) { + reader.getVarInt(4, 18) != null + || reader.getString(4, 5, 1, 2)?.contains("/h/") == true + } Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { if (sentWithLocation && messageIndicatorsConfig.contains("location_indicator")) { - Image( - imageVector = Icons.Default.LocationOn, - colorFilter = ColorFilter.tint(Color.Green), - contentDescription = null, - modifier = Modifier.size(15.dp) - ) + GradientIcon(Icons.Default.LocationOn, locationBrush) } if (messageIndicatorsConfig.contains("platform_indicator")) { - Image( - imageVector = when { - sentFromWebApp -> Icons.Default.Laptop - sentFromIosDevice -> appleLogo - else -> Icons.Default.Android - }, - colorFilter = ColorFilter.tint(Color.Green), - contentDescription = null, - modifier = Modifier.size(15.dp) - ) + val (platformIcon, platformBrush) = when { + sentFromWebApp -> Icons.Default.Laptop to webBrush + sentFromIosDevice -> appleLogo to appleBrush + else -> Icons.Default.Android to androidBrush + } + GradientIcon(platformIcon, platformBrush) } if (hasEncryption && messageIndicatorsConfig.contains("encryption_indicator")) { - Image( - imageVector = Icons.Default.Lock, - colorFilter = ColorFilter.tint(Color.Green), - contentDescription = null, - modifier = Modifier.size(15.dp) - ) + GradientIcon(Icons.Default.Lock, lockBrush) } if (sentUsingDirectorMode && messageIndicatorsConfig.contains("director_mode_indicator")) { - Image( - imageVector = Icons.Default.Edit, - colorFilter = ColorFilter.tint(Color.Red), - contentDescription = null, - modifier = Modifier.size(15.dp) - ) + GradientIcon(Icons.Default.Edit, directorBrush) + } + if (sentFromMemories && messageIndicatorsConfig.contains("memories_indicator")) { + GradientIcon(Icons.Default.HistoryEdu, memoriesBrush) } if (sentUsingOvfEditor && messageIndicatorsConfig.contains("ovf_editor_indicator")) { - Text( + GradientText( text = "OVF", - color = Color.Red, + brush = ovfBrush, fontWeight = FontWeight.ExtraBold, - fontSize = 10.sp, + fontSize = 11.sp ) } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/PinConversations.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/PinConversations.kt index 03e946ca..b54971ee 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/PinConversations.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/PinConversations.kt @@ -7,47 +7,112 @@ import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.Hooker import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hookConstructor -import me.eternal.purrfectsnap.core.util.ktx.getObjectField +import me.eternal.purrfectsnap.core.util.ktx.getObjectFieldOrNull import me.eternal.purrfectsnap.core.util.ktx.setObjectField import me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID +import me.eternal.purrfectsnap.mapper.impl.CallbackMapper +import java.util.Collections class PinConversations : MessagingRuleFeature("PinConversations", MessagingRuleType.PIN_CONVERSATION) { + companion object { + // 3-year offset for persistent local conversation sorting + private const val PIN_OFFSET = 100000000000L + } + + private fun forcePinsInFeed(entries: ArrayList) { + val now = System.currentTimeMillis() + // Capture stable timestamp once to prevent jitter during the sweep + val stableTimestamp = now + PIN_OFFSET + + entries.forEach { entry -> + val conversationIdObject = entry.getObjectFieldOrNull("mConversationId") ?: return@forEach + runCatching { + val conversationUUID = SnapUUID(conversationIdObject) + if (getState(conversationUUID.toString())) { + // Apply identical timestamp lead to all pinned items + entry.setObjectField("mPinnedTimestampMs", stableTimestamp) + } else { + // Reset timestamp if it's currently a "Future" timestamp but shouldn't be pinned + val currentTs = entry.getObjectFieldOrNull("mPinnedTimestampMs") as? Long ?: 0L + if (currentTs > now + (PIN_OFFSET / 2)) { + entry.setObjectField("mPinnedTimestampMs", now) + } + } + } + } + + // Manual sort to ensure stable UI transition and prevent list jumping + runCatching { + Collections.sort(entries) { a, b -> + val tsA = a.getObjectFieldOrNull("mPinnedTimestampMs") as? Long ?: 0L + val tsB = b.getObjectFieldOrNull("mPinnedTimestampMs") as? Long ?: 0L + tsB.compareTo(tsA) + } + } + } + override fun init() { if (!context.config.messaging.unlimitedConversationPinning.get()) return + // Intercept native pinning requests and bypass server-side limits context.classCache.feedManager.hook("setPinnedConversationStatus", HookStage.BEFORE) { param -> val conversationUUID = SnapUUID(param.arg(0)) val isPinned = param.arg(1).toString() == "PINNED" setState(conversationUUID.toString(), isPinned) + + // Callback forcing to suppress "Can't pin conversation" errors for both PIN and UNPIN val callback = param.arg(2) mutableSetOf<() -> Unit>().apply { - addAll(Hooker.ephemeralHookObjectMethod(callback::class.java, callback,"onSuccess", HookStage.BEFORE) { + addAll(Hooker.ephemeralHookObjectMethod(callback::class.java, callback, "onSuccess", HookStage.BEFORE) { forEach { it() } }) - addAll(Hooker.ephemeralHookObjectMethod(callback::class.java, callback,"onError", HookStage.BEFORE) { methodParam -> + addAll(Hooker.ephemeralHookObjectMethod(callback::class.java, callback, "onError", HookStage.BEFORE) { methodParam -> methodParam.setResult(null) + // Manually trigger success to bypass server-side limit rejections callback::class.java.getDeclaredMethod("onSuccess").invoke(callback) }) } } - context.classCache.conversation.hookConstructor(HookStage.AFTER) { param -> - val instance = param.thisObject() - val conversationUUID = SnapUUID(instance.getObjectField("mConversationId")) - if (getState(conversationUUID.toString())) { - instance.setObjectField("mPinnedTimestampMs", 1L) + // Active feed sweep to ensure pinned conversations remain at the top + context.mappings.useMapper(CallbackMapper::class) { + val callbackMap = callbacks.getAsMap().orEmpty() + callbackMap.entries.forEach { (_, className) -> + val clazz = runCatching { findClass(className!!) }.getOrNull() ?: return@forEach + clazz.methods.forEach { method -> + if (method.name.startsWith("on") && method.name.endsWith("Complete") && method.parameterTypes.any { it == ArrayList::class.java }) { + clazz.hook(method.name, HookStage.BEFORE) { param -> + (param.args().firstOrNull { it is ArrayList<*> } as? ArrayList)?.let { forcePinsInFeed(it) } + } + } + } } } + // Apply pinning lead to newly created conversation objects + context.classCache.conversation.hookConstructor(HookStage.AFTER) { param -> + val instance = param.thisObject() + val conversationIdObject = instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor + runCatching { + val conversationUUID = SnapUUID(conversationIdObject) + if (getState(conversationUUID.toString())) { + instance.setObjectField("mPinnedTimestampMs", System.currentTimeMillis() + PIN_OFFSET) + } + } + } + + // Apply pinning lead to newly created feed entry objects context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param -> val instance = param.thisObject() - val conversationUUID = SnapUUID(instance.getObjectField("mConversationId") ?: return@hookConstructor) - val isPinned = getState(conversationUUID.toString()) - if (isPinned) { - instance.setObjectField("mPinnedTimestampMs", 1L) + val conversationIdObject = instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor + runCatching { + val conversationUUID = SnapUUID(conversationIdObject) + if (getState(conversationUUID.toString())) { + instance.setObjectField("mPinnedTimestampMs", System.currentTimeMillis() + PIN_OFFSET) + } } } } override fun getRuleState() = RuleState.WHITELIST -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/ui/ViewAppearanceHelper.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/ui/ViewAppearanceHelper.kt index 731cfff5..d606f6f6 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/ui/ViewAppearanceHelper.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/ui/ViewAppearanceHelper.kt @@ -137,6 +137,8 @@ fun View.onAttachChange(onAttach: (View.OnAttachStateChangeListener) -> Unit = { fun View.hideViewCompletely() { fun hide() { + if (visibility == View.GONE && layoutParams?.width == 0 && layoutParams?.height == 0) return + isEnabled = false visibility = View.GONE setWillNotDraw(true) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/media/HttpServer.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/media/HttpServer.kt index c42a3c6f..23b0cabb 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/media/HttpServer.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/media/HttpServer.kt @@ -12,11 +12,12 @@ import java.net.SocketException import java.util.Locale import java.util.StringTokenizer import java.util.concurrent.ConcurrentHashMap +import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlin.random.Random class HttpServer( - private val timeout: Int = 10000 + private val timeout: Int = 15000 // Optimized: 15s Middle Ground ) { private fun newRandomPort() = Random.nextInt(10000, 65535) @@ -53,18 +54,24 @@ class HttpServer( AbstractLogger.directDebug("Starting http server on port $port") for (i in 0..5) { try { - serverSocket = ServerSocket(port) + serverSocket = ServerSocket(port).apply { + soTimeout = timeout + 5000 + } break } catch (e: Throwable) { AbstractLogger.directError("failed to start http server on port $port", e) port = newRandomPort() } } - continuation.resumeWith(Result.success(if (serverSocket == null) null.also { + + if (serverSocket == null) { + continuation.resume(null) return@launch - } else this@HttpServer)) + } + + continuation.resume(this@HttpServer) - while (!serverSocket!!.isClosed) { + while (isActive && serverSocket?.isClosed == false) { try { val socket = serverSocket!!.accept() timeoutJob?.cancel() @@ -77,14 +84,12 @@ class HttpServer( socketJob?.cancel() socket.close() serverSocket?.close() - }.onFailure { - AbstractLogger.directError("failed to close socket", it) } } } } catch (e: SocketException) { - AbstractLogger.directDebug("http server timed out") - break; + AbstractLogger.directDebug("http server timed out or closed") + break } catch (e: Throwable) { AbstractLogger.directError("failed to handle request", e) } @@ -96,8 +101,11 @@ class HttpServer( } fun close() { - runCatching { - serverSocket?.close() + coroutineScope.launch { + runCatching { + serverSocket?.close() + socketJob?.cancel() + } } } @@ -133,19 +141,21 @@ class HttpServer( val reader = BufferedReader(InputStreamReader(socket.getInputStream())) val outputStream = socket.getOutputStream() val writer = PrintWriter(outputStream) - val line = reader.readLine() ?: return + val line = runCatching { reader.readLine() }.getOrNull() ?: return + fun close() { runCatching { reader.close() writer.close() outputStream.close() socket.close() - }.onFailure { - AbstractLogger.directError("failed to close socket", it) } } + val parse = StringTokenizer(line) + if (!parse.hasMoreTokens()) { close(); return } val method = parse.nextToken().uppercase(Locale.getDefault()) + if (!parse.hasMoreTokens()) { close(); return } var fileRequested = parse.nextToken().lowercase(Locale.getDefault()) AbstractLogger.directDebug("[http-server:${port}] $method $fileRequested") diff --git a/gradle.properties b/gradle.properties index 5960e246..fbd09353 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,8 +7,8 @@ org.gradle.configuration-cache=true org.gradle.configuration-cache.problems=warn nativeAbis=arm64-v8a -APP_VERSION_NAME=1.6.9 -APP_VERSION_CODE=325 +APP_VERSION_NAME=1.7.1 +APP_VERSION_CODE=327 debug_build_hash=18fe2a814d0e2eb5 psIntegrityPinnedSha256= EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c diff --git a/settings.gradle.kts b/settings.gradle.kts index 988894e9..a9256059 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -115,8 +115,8 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven { url = uri("https://api.xposed.info/") } maven { url = uri("https://jitpack.io") } + maven { url = uri("https://api.xposed.info/") } } }