v1.1.0: Stable!
This commit is contained in:
@@ -110,13 +110,35 @@ android {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
storeFile = File(System.getProperty("user.home"), ".android/purrfectsnap-release.keystore")
|
||||
storePassword = providers.gradleProperty("PS_RELEASE_STORE_PASSWORD").orNull
|
||||
keyAlias = providers.gradleProperty("PS_RELEASE_KEY_ALIAS").orNull
|
||||
keyPassword = providers.gradleProperty("PS_RELEASE_KEY_PASSWORD").orNull
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
val autoCertSha = providers.provider {
|
||||
computeKeystoreCertSha256(
|
||||
File(System.getProperty("user.home"), ".android/debug.keystore"),
|
||||
storePass = "android",
|
||||
keyAlias = "androiddebugkey"
|
||||
).orEmpty()
|
||||
val releaseStore = File(System.getProperty("user.home"), ".android/purrfectsnap-release.keystore")
|
||||
val releaseStorePass = providers.gradleProperty("PS_RELEASE_STORE_PASSWORD").orNull
|
||||
val releaseKeyAlias = providers.gradleProperty("PS_RELEASE_KEY_ALIAS").orNull
|
||||
val releaseKeyPass = providers.gradleProperty("PS_RELEASE_KEY_PASSWORD").orNull
|
||||
if (!releaseStorePass.isNullOrBlank() && !releaseKeyAlias.isNullOrBlank()) {
|
||||
computeKeystoreCertSha256(
|
||||
releaseStore,
|
||||
storePass = releaseStorePass,
|
||||
keyAlias = releaseKeyAlias,
|
||||
keyPass = releaseKeyPass ?: releaseStorePass
|
||||
)
|
||||
} else {
|
||||
computeKeystoreCertSha256(
|
||||
File(System.getProperty("user.home"), ".android/debug.keystore"),
|
||||
storePass = "android",
|
||||
keyAlias = "androiddebugkey"
|
||||
)
|
||||
}.orEmpty()
|
||||
}
|
||||
val expectedCertSha256 = providers.gradleProperty("EXPECTED_CERT_SHA256")
|
||||
.orElse(providers.gradleProperty("psExpectedCertSha256"))
|
||||
@@ -135,6 +157,7 @@ android {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles += file("proguard-rules.pro")
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
debug {
|
||||
(properties["debug_flavor"] == null).also {
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
android:usesCleartextTraffic="true"
|
||||
android:label="@string/app_name"
|
||||
tools:targetApi="34"
|
||||
android:allowBackup="true"
|
||||
android:hasFragileUserData="true"
|
||||
android:allowBackup="false"
|
||||
android:hasFragileUserData="false"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round">
|
||||
@@ -58,11 +58,12 @@
|
||||
<activity android:name=".bridge.ForceStartActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false" />
|
||||
android:exported="true" />
|
||||
<activity android:name=".bridge.BiometricPromptActivity"
|
||||
android:theme="@style/BiometricPromptTheme"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false" />
|
||||
android:exported="true"
|
||||
android:permission="com.snapchat.android.permission.UPDATE_STICKER_INDEX" />
|
||||
<receiver android:name=".StreaksReminder" />
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
||||
@@ -104,10 +104,10 @@ class RemoteFileHandleManager(
|
||||
File(userImportFolder, name.substringAfterLast("/"))
|
||||
)
|
||||
}
|
||||
FileHandleScope.COMPOSER -> {
|
||||
FileHandleScope.VALDI -> {
|
||||
AssetFileHandle(
|
||||
context,
|
||||
"composer/${name.substringAfterLast("/")}"
|
||||
"valdi/${name.substringAfterLast("/")}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,13 +431,15 @@ class DownloadProcessor (
|
||||
return@let
|
||||
}
|
||||
}
|
||||
callbackOnFailure(translation["already_downloaded_toast"], null)
|
||||
callbackOnFailure(translation["already_downloaded_toast"])
|
||||
return@launch
|
||||
} else {
|
||||
callbackOnFailure(translation["already_queued_toast"], null)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
callbackOnProgress(translation["download_started_toast"])
|
||||
remoteSideContext.log.debug("downloading media")
|
||||
val pendingTask = remoteSideContext.taskManager.createPendingTask(
|
||||
Task(
|
||||
|
||||
@@ -111,6 +111,11 @@ class FFMpegProcessor(
|
||||
}, { onStatistics(it) }, Executors.newSingleThreadExecutor())
|
||||
}
|
||||
|
||||
private fun isMediaCodecFailure(output: String): Boolean {
|
||||
val lower = output.lowercase()
|
||||
return lower.contains("mediacodec") || lower.contains("h264_mediacodec") || lower.contains("amediacodec")
|
||||
}
|
||||
|
||||
suspend fun execute(args: Request) {
|
||||
// load ffmpeg native sync to avoid native crash
|
||||
synchronized(this) { FFmpegKit.listSessions() }
|
||||
@@ -222,6 +227,20 @@ class FFMpegProcessor(
|
||||
}
|
||||
}
|
||||
outputArguments += args.output.absolutePath
|
||||
newFFMpegTask(globalArguments, inputArguments, outputArguments)
|
||||
try {
|
||||
newFFMpegTask(globalArguments, inputArguments, outputArguments)
|
||||
} catch (e: Exception) {
|
||||
val output = e.message.orEmpty()
|
||||
val usingMediaCodec = outputArguments["-c:v"] == "h264_mediacodec"
|
||||
val canRetry = ffmpegOptions.customVideoCodec.get().isEmpty()
|
||||
if (usingMediaCodec && canRetry && isMediaCodecFailure(output)) {
|
||||
logManager.warn("MediaCodec failed, retrying with libx264", TAG)
|
||||
outputArguments -= "-c:v"
|
||||
outputArguments += "-c:v" to "libx264"
|
||||
newFFMpegTask(globalArguments, inputArguments, outputArguments)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package me.eternal.purrfectsnap.setup.patch
|
||||
|
||||
import com.google.gson.JsonParser
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.random.Random
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
@@ -29,7 +30,7 @@ class AutoPatchServer(
|
||||
|
||||
fun fetchLatestSnapchatApk(): LatestApk? {
|
||||
val request = Request.Builder()
|
||||
.url("https://api.github.com/repos/particle-box/auto-patch-server/releases/latest")
|
||||
.url("https://api.github.com/repos/particle-box/download-snap/releases/latest")
|
||||
.build()
|
||||
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
@@ -47,9 +48,9 @@ class AutoPatchServer(
|
||||
name to downloadUrl
|
||||
}
|
||||
|
||||
val selected = apkAssets.firstOrNull { it.first.contains("snapchat", ignoreCase = true) }
|
||||
?: apkAssets.firstOrNull()
|
||||
?: return null
|
||||
val nonPrimary = apkAssets.filterNot { it.first.contains("snapchat", ignoreCase = true) }
|
||||
val selectionPool = if (nonPrimary.isNotEmpty()) nonPrimary else apkAssets
|
||||
val selected = selectionPool.random(Random.Default)
|
||||
|
||||
return LatestApk(
|
||||
tagName = tagName,
|
||||
@@ -59,4 +60,3 @@ class AutoPatchServer(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ data class PatchConfig(
|
||||
) {
|
||||
data class LSPConfig(
|
||||
var API_CODE: Int = 93,
|
||||
var VERSION_CODE: Int = 360,
|
||||
var VERSION_NAME: String = "0.5.1",
|
||||
var CORE_VERSION_CODE: Int = 6649,
|
||||
var CORE_VERSION_NAME: String = "1.8.5",
|
||||
var VERSION_CODE: Int = 430,
|
||||
var VERSION_NAME: String = "0.7",
|
||||
var CORE_VERSION_CODE: Int = 7137,
|
||||
var CORE_VERSION_NAME: String = "1.10.1",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.work.WorkerParameters
|
||||
import me.eternal.purrfectsnap.R
|
||||
import me.eternal.purrfectsnap.ui.manager.MainActivity
|
||||
import me.eternal.purrfectsnap.ui.manager.data.Updater
|
||||
import me.eternal.purrfectsnap.ui.manager.data.Updater.Channel
|
||||
|
||||
class UpdateCheckWorker(
|
||||
private val appContext: Context,
|
||||
@@ -24,7 +25,11 @@ class UpdateCheckWorker(
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return try {
|
||||
val latestRelease = Updater.latestRelease
|
||||
val channel = when (inputData.getString("update_channel")) {
|
||||
"prerelease" -> Channel.PRERELEASE
|
||||
else -> Channel.STABLE
|
||||
}
|
||||
val latestRelease = Updater.getLatestRelease(channel)
|
||||
if (latestRelease != null) {
|
||||
showUpdateNotification(latestRelease.versionName)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
@@ -174,7 +175,22 @@ class MainActivity : ComponentActivity() {
|
||||
} else {
|
||||
PaddingValues(0.dp)
|
||||
}
|
||||
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
navigation.NavContent(contentPadding, startDestination)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.fillMaxWidth()
|
||||
.height(statusBarHeight + 24.dp)
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF241F52),
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
if (!isFullscreen) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -332,8 +332,7 @@ class Navigation(
|
||||
}
|
||||
val horizontalInset = 2.dp
|
||||
val indicatorWidth = (with(density) { itemWidthPx.toDp() } - horizontalInset * 2)
|
||||
.coerceAtLeast(70.dp)
|
||||
.coerceAtMost(with(density) { itemWidthPx.toDp() })
|
||||
.coerceAtLeast(0.dp)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
||||
@@ -19,8 +19,10 @@ import me.eternal.purrfectsnap.ui.manager.pages.TasksRootSection
|
||||
import me.eternal.purrfectsnap.ui.manager.pages.features.FeaturesRootSection
|
||||
import me.eternal.purrfectsnap.ui.manager.pages.features.ManageRuleFeature
|
||||
import me.eternal.purrfectsnap.ui.manager.pages.home.HomeLogs
|
||||
import me.eternal.purrfectsnap.ui.manager.pages.home.HomeAbout
|
||||
import me.eternal.purrfectsnap.ui.manager.pages.home.HomeRootSection
|
||||
import me.eternal.purrfectsnap.ui.manager.pages.home.HomeSettings
|
||||
import me.eternal.purrfectsnap.ui.manager.pages.home.RetroGameScreen
|
||||
import me.eternal.purrfectsnap.ui.manager.pages.location.BetterLocationRoot
|
||||
import me.eternal.purrfectsnap.ui.manager.pages.scripting.ScriptingRootSection
|
||||
import me.eternal.purrfectsnap.ui.manager.pages.social.LoggedStories
|
||||
@@ -67,8 +69,8 @@ class Routes(
|
||||
var friendTrackerConfigJsonForImport: String? = null
|
||||
var onRuleImported: (() -> Unit)? = null
|
||||
|
||||
val configImportConfirmation = route(RouteInfo(CONFIG_IMPORT_CONFIRMATION_ROUTE), me.eternal.purrfectsnap.ui.manager.pages.features.ConfigImportConfirmationScreen())
|
||||
val configExportSummary = route(RouteInfo(CONFIG_EXPORT_SUMMARY_ROUTE), me.eternal.purrfectsnap.ui.manager.pages.features.ConfigExportSummaryScreen())
|
||||
val configImportConfirmation = route(RouteInfo(CONFIG_IMPORT_CONFIRMATION_ROUTE, hasOwnTopBar = true), me.eternal.purrfectsnap.ui.manager.pages.features.ConfigImportConfirmationScreen())
|
||||
val configExportSummary = route(RouteInfo(CONFIG_EXPORT_SUMMARY_ROUTE, hasOwnTopBar = true), me.eternal.purrfectsnap.ui.manager.pages.features.ConfigExportSummaryScreen())
|
||||
|
||||
val tasks = route(RouteInfo("tasks", icon = Icons.Default.TaskAlt, primary = true, hasOwnTopBar = true), TasksRootSection())
|
||||
|
||||
@@ -76,6 +78,8 @@ class Routes(
|
||||
val manageRuleFeature = route(RouteInfo("manage_rule_feature/?rule_type={rule_type}", hasOwnTopBar = true), ManageRuleFeature()).parent(features)
|
||||
|
||||
val home = route(RouteInfo("home", icon = Icons.Default.Home, primary = true, hasOwnTopBar = true), HomeRootSection())
|
||||
val about = route(RouteInfo("home_about", hasOwnTopBar = true), HomeAbout()).parent(home)
|
||||
val retroGame = route(RouteInfo("retro_game", hasOwnTopBar = true), RetroGameScreen()).parent(home)
|
||||
val settings = route(RouteInfo("home_settings", hasOwnTopBar = true), HomeSettings()).parent(home)
|
||||
val homeLogs = route(RouteInfo("home_logs", hasOwnTopBar = true), HomeLogs()).parent(home)
|
||||
val loggerHistory = route(RouteInfo("logger_history", hasOwnTopBar = true), LoggerHistoryRoot()).parent(home)
|
||||
@@ -87,7 +91,7 @@ class Routes(
|
||||
val friendTrackerCatalog = route(RouteInfo("friend_tracker_catalog", hasOwnTopBar = true), FriendTrackerCatalog())
|
||||
val manageFriendTrackerRepos = route(RouteInfo("manage_friend_tracker_repos", hasOwnTopBar = true), ManageFriendTrackerReposSection())
|
||||
|
||||
val fileImports = route(RouteInfo("file_imports"), FileImportsRoot()).parent(home)
|
||||
val fileImports = route(RouteInfo("file_imports", hasOwnTopBar = true), FileImportsRoot()).parent(home)
|
||||
val manageRepos = route(RouteInfo("manage_repos/?type={type}"), ManageReposSection())
|
||||
|
||||
val social = route(RouteInfo("social", icon = Icons.Default.Group, primary = true, hasOwnTopBar = true), SocialRootSection())
|
||||
|
||||
@@ -62,7 +62,9 @@ fun AestheticDialog(
|
||||
loading: Boolean = false,
|
||||
opaque: Boolean = false,
|
||||
showCloseButton: Boolean = true,
|
||||
confirmEnabled: Boolean = true
|
||||
confirmEnabled: Boolean = true,
|
||||
showIcon: Boolean = true,
|
||||
showTitle: Boolean = true
|
||||
) {
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) { visible = true }
|
||||
@@ -94,28 +96,32 @@ fun AestheticDialog(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(62.dp)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.3f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.28f)
|
||||
)
|
||||
if (showIcon) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(62.dp)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.3f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.28f)
|
||||
)
|
||||
),
|
||||
CircleShape
|
||||
),
|
||||
CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = Color.White, modifier = Modifier.size(30.dp))
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = Color.White, modifier = Modifier.size(30.dp))
|
||||
}
|
||||
}
|
||||
if (showTitle && title.isNotBlank()) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.ExtraBold),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.ExtraBold),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
if (text.isNotBlank()) {
|
||||
Text(
|
||||
text = text,
|
||||
@@ -130,25 +136,27 @@ fun AestheticDialog(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally)
|
||||
) {
|
||||
if (dismissButtonText != null && onDismiss != null) {
|
||||
Button(
|
||||
onClick = onDismiss,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White.copy(alpha = 0.08f),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) { Text(dismissButtonText) }
|
||||
}
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
enabled = confirmEnabled && !loading,
|
||||
onClick = onDismiss,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.34f),
|
||||
containerColor = Color.White.copy(alpha = 0.08f),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
if (loading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
) { Text(dismissButtonText) }
|
||||
}
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
enabled = confirmEnabled && !loading,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.34f),
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.22f),
|
||||
disabledContentColor = Color.White.copy(alpha = 0.75f)
|
||||
)
|
||||
) {
|
||||
if (loading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = Color.White
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package me.eternal.purrfectsnap.ui.manager.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import com.tonyodev.fetch2.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -15,13 +15,14 @@ import java.io.FileOutputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
object UpdateDownloader {
|
||||
private const val TAG = "UpdateDownloader"
|
||||
private var fetch: Fetch? = null
|
||||
private var listener: FetchListener? = null
|
||||
|
||||
private fun getInstance(context: Context): Fetch {
|
||||
private fun getInstance(context: RemoteSideContext): Fetch {
|
||||
fetch?.let { return it }
|
||||
fetch = run {
|
||||
val fetchConfiguration = FetchConfiguration.Builder(context.applicationContext)
|
||||
val fetchConfiguration = FetchConfiguration.Builder(context.androidContext)
|
||||
.setDownloadConcurrentLimit(3)
|
||||
.build()
|
||||
Fetch.getInstance(fetchConfiguration)
|
||||
@@ -56,14 +57,43 @@ object UpdateDownloader {
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveDownloadedApk(
|
||||
remoteContext: RemoteSideContext,
|
||||
downloadedFile: File
|
||||
): File {
|
||||
val context = remoteContext.androidContext
|
||||
if (downloadedFile.extension.equals("zip", ignoreCase = true)) {
|
||||
val unzipDir = File(context.externalCacheDir, "update")
|
||||
if (unzipDir.exists()) unzipDir.deleteRecursively()
|
||||
unzipDir.mkdirs()
|
||||
remoteContext.log.info(
|
||||
"Extracting update archive ${downloadedFile.absolutePath} -> ${unzipDir.absolutePath}",
|
||||
TAG
|
||||
)
|
||||
unzip(downloadedFile, unzipDir)
|
||||
return unzipDir.walk().firstOrNull { it.isFile && it.extension.equals("apk", true) }
|
||||
?: throw IllegalStateException("No APK found in the downloaded archive")
|
||||
}
|
||||
|
||||
if (!downloadedFile.extension.equals("apk", ignoreCase = true)) {
|
||||
remoteContext.log.warn(
|
||||
"Downloaded file is not an APK or ZIP (${downloadedFile.name}); attempting installation anyway.",
|
||||
TAG
|
||||
)
|
||||
}
|
||||
return downloadedFile
|
||||
}
|
||||
|
||||
fun downloadAndInstall(
|
||||
context: Context,
|
||||
remoteContext: RemoteSideContext,
|
||||
downloadUrl: String,
|
||||
fileName: String,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
val fetch = getInstance(context)
|
||||
val context = remoteContext.androidContext
|
||||
val fetch = getInstance(remoteContext)
|
||||
val filePath = File(context.externalCacheDir, fileName).path
|
||||
remoteContext.log.info("Starting update download from $downloadUrl -> $filePath", TAG)
|
||||
val request = Request(downloadUrl, filePath).apply {
|
||||
priority = Priority.HIGH
|
||||
networkType = NetworkType.ALL
|
||||
@@ -72,6 +102,7 @@ object UpdateDownloader {
|
||||
listener = object : AbstractFetchListener() {
|
||||
override fun onAdded(download: Download) {
|
||||
downloadState.value = DownloadState.DOWNLOADING
|
||||
remoteContext.log.info("Queued update download: ${download.file}", TAG)
|
||||
}
|
||||
|
||||
override fun onQueued(download: Download, waitingOnNetwork: Boolean) {
|
||||
@@ -85,26 +116,36 @@ object UpdateDownloader {
|
||||
|
||||
override fun onCompleted(download: Download) {
|
||||
downloadState.value = DownloadState.COMPLETED
|
||||
Toast.makeText(context, "Download completed", Toast.LENGTH_SHORT).show()
|
||||
runCatching {
|
||||
val downloadedFile = File(download.file)
|
||||
val unzipDir = File(context.externalCacheDir, "update")
|
||||
if (unzipDir.exists()) {
|
||||
unzipDir.deleteRecursively()
|
||||
}
|
||||
unzipDir.mkdirs()
|
||||
unzip(downloadedFile, unzipDir)
|
||||
val apkFile = unzipDir.walk().find { it.isFile && it.extension == "apk" }
|
||||
?: throw Exception("No APK found in the downloaded file")
|
||||
val uri = FileProvider.getUriForFile(context, "me.eternal.purrfectsnap.fileprovider", apkFile)
|
||||
remoteContext.log.info(
|
||||
"Download completed -> ${downloadedFile.absolutePath} (${downloadedFile.length()} bytes)",
|
||||
TAG
|
||||
)
|
||||
Toast.makeText(context, "Download completed", Toast.LENGTH_SHORT).show()
|
||||
val apkFile = resolveDownloadedApk(remoteContext, downloadedFile)
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
apkFile
|
||||
)
|
||||
val installIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
remoteContext.log.info("Launching installer for ${apkFile.absolutePath}", TAG)
|
||||
context.startActivity(installIntent)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
delay(30_000)
|
||||
runCatching { downloadedFile.delete() }
|
||||
apkFile.parentFile
|
||||
?.takeIf { it.name == "update" }
|
||||
?.let { dir -> runCatching { dir.deleteRecursively() } }
|
||||
remoteContext.log.info("Cleaned downloaded update files", TAG)
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
Toast.makeText(context, "Failed to install update. Check logs for more details.", Toast.LENGTH_SHORT).show()
|
||||
remoteContext.log.error("Failed to install downloaded update", it, TAG)
|
||||
downloadState.value = DownloadState.FAILED
|
||||
}
|
||||
fetch.removeListener(this)
|
||||
@@ -117,6 +158,8 @@ object UpdateDownloader {
|
||||
override fun onError(download: Download, error: Error, throwable: Throwable?) {
|
||||
downloadState.value = DownloadState.FAILED
|
||||
Toast.makeText(context, "Download failed: $error", Toast.LENGTH_SHORT).show()
|
||||
throwable?.let { remoteContext.log.error("Update download failed: $error", it, TAG) }
|
||||
?: remoteContext.log.error("Update download failed: $error", TAG)
|
||||
fetch.removeListener(this)
|
||||
scope.launch {
|
||||
delay(2000)
|
||||
|
||||
@@ -8,13 +8,34 @@ import okhttp3.Request
|
||||
|
||||
|
||||
object Updater {
|
||||
enum class Channel { STABLE, PRERELEASE }
|
||||
|
||||
data class LatestRelease(
|
||||
val versionName: String,
|
||||
val releaseUrl: String,
|
||||
val workflowId: Long?,
|
||||
val assetDownloads: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
private fun fetchLatestRelease() = runCatching {
|
||||
private fun isVersionGreater(version: String, current: String): Boolean {
|
||||
fun segments(raw: String) = raw
|
||||
.split(Regex("[^0-9]+"))
|
||||
.filter { it.isNotBlank() }
|
||||
.map { it.toIntOrNull() ?: 0 }
|
||||
|
||||
val v = segments(version)
|
||||
val c = segments(current)
|
||||
val size = maxOf(v.size, c.size)
|
||||
for (i in 0 until size) {
|
||||
val vi = v.getOrElse(i) { 0 }
|
||||
val ci = c.getOrElse(i) { 0 }
|
||||
if (vi > ci) return true
|
||||
if (vi < ci) return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun fetchLatestRelease(channel: Channel) = runCatching {
|
||||
val endpoint = Request.Builder().url("https://api.github.com/repos/particle-box/PurrfectSnap/releases").build()
|
||||
val response = OkHttpClient().newCall(endpoint).execute()
|
||||
|
||||
@@ -24,17 +45,45 @@ object Updater {
|
||||
if (it.size() == 0) throw Throwable("No releases found")
|
||||
}
|
||||
|
||||
val latestRelease = releases.get(0).asJsonObject
|
||||
val latestVersion = latestRelease.getAsJsonPrimitive("tag_name").asString
|
||||
if (latestVersion.removePrefix("v") == BuildConfig.VERSION_NAME) return@runCatching null
|
||||
val currentVersion = BuildConfig.VERSION_NAME
|
||||
val latestRelease = releases.mapNotNull { it.asJsonObject }.firstOrNull { release ->
|
||||
if (release.get("draft")?.asBoolean != false) return@firstOrNull false
|
||||
val matchesChannel = when (channel) {
|
||||
Channel.STABLE -> release.get("prerelease")?.asBoolean == false
|
||||
Channel.PRERELEASE -> release.get("prerelease")?.asBoolean == true
|
||||
}
|
||||
if (!matchesChannel) return@firstOrNull false
|
||||
val latestVersion = release.getAsJsonPrimitive("tag_name")?.asString?.removePrefix("v") ?: return@firstOrNull false
|
||||
isVersionGreater(latestVersion, currentVersion)
|
||||
} ?: throw Throwable("No matching releases found for $channel channel")
|
||||
|
||||
val latestVersion = latestRelease.getAsJsonPrimitive("tag_name").asString.removePrefix("v")
|
||||
if (latestVersion == BuildConfig.VERSION_NAME) return@runCatching null
|
||||
val assets = latestRelease.getAsJsonArray("assets")?.mapNotNull { element ->
|
||||
val obj = element.asJsonObject
|
||||
val name = obj.getAsJsonPrimitive("name")?.asString?.lowercase() ?: return@mapNotNull null
|
||||
val url = obj.getAsJsonPrimitive("browser_download_url")?.asString ?: return@mapNotNull null
|
||||
name to url
|
||||
} ?: emptyList()
|
||||
|
||||
val assetDownloads = buildMap<String, String> {
|
||||
assets.forEach { (name, url) ->
|
||||
when {
|
||||
name.contains("arm64") || name.contains("armv8") -> put("arm64", url)
|
||||
name.contains("armeabi") || name.contains("armv7") || name.contains("arm32") -> put("armv7", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LatestRelease(
|
||||
versionName = latestVersion,
|
||||
releaseUrl = endpoint.url.toString().replace("api.", "").replace("repos/", ""),
|
||||
workflowId = null
|
||||
releaseUrl = latestRelease.getAsJsonPrimitive("html_url")?.asString
|
||||
?: endpoint.url.toString().replace("api.", "").replace("repos/", ""),
|
||||
workflowId = null,
|
||||
assetDownloads = assetDownloads
|
||||
)
|
||||
}.onFailure {
|
||||
AbstractLogger.directError("Failed to fetch latest release", it)
|
||||
AbstractLogger.directError("Failed to fetch latest release ($channel)", it)
|
||||
}.getOrNull()
|
||||
|
||||
private fun fetchLatestDebugCI() = runCatching {
|
||||
@@ -60,7 +109,15 @@ object Updater {
|
||||
AbstractLogger.directError("Failed to fetch latest debug CI", it)
|
||||
}.getOrNull()
|
||||
|
||||
val latestRelease by lazy {
|
||||
if (BuildConfig.DEBUG) fetchLatestDebugCI() else fetchLatestRelease()
|
||||
private val cache = mutableMapOf<Channel, LatestRelease?>()
|
||||
|
||||
fun getLatestRelease(channel: Channel): LatestRelease? {
|
||||
return cache.getOrPut(channel) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
fetchLatestDebugCI() ?: fetchLatestRelease(channel)
|
||||
} else {
|
||||
fetchLatestRelease(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.net.Uri
|
||||
import android.text.format.Formatter
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -20,19 +22,24 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AttachFile
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
import androidx.compose.material.icons.filled.Upload
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
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
|
||||
@@ -45,6 +52,7 @@ import me.eternal.purrfectsnap.common.ui.AsyncUpdateDispatcher
|
||||
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState
|
||||
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableStateList
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.ActivityLauncherHelper
|
||||
import me.eternal.purrfectsnap.ui.util.openFile
|
||||
@@ -58,44 +66,75 @@ class FileImportsRoot: Routes.Route() {
|
||||
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImportFab(onClick: () -> Unit) {
|
||||
val shape = RoundedCornerShape(18.dp)
|
||||
val border = Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.7f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.6f)
|
||||
)
|
||||
)
|
||||
val fill = Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.35f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.28f)
|
||||
)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.shadow(
|
||||
elevation = 12.dp,
|
||||
shape = shape,
|
||||
ambientColor = PurrfectPalette.glowSecondary.copy(alpha = 0.2f),
|
||||
spotColor = PurrfectPalette.glowPrimary.copy(alpha = 0.25f)
|
||||
)
|
||||
.clip(shape)
|
||||
.background(fill)
|
||||
.border(1.dp, border, shape)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Upload, contentDescription = null, tint = Color.White)
|
||||
Text(
|
||||
text = translation["import_file_button"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override val floatingActionButton: @Composable () -> Unit = {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Row {
|
||||
ExtendedFloatingActionButton(
|
||||
icon = {
|
||||
Icon(Icons.Default.Upload, contentDescription = null)
|
||||
},
|
||||
text = {
|
||||
Text(translation["import_file_button"])
|
||||
},
|
||||
onClick = {
|
||||
context.coroutineScope.launch {
|
||||
activityLauncherHelper.openFile { filePath ->
|
||||
val fileUri = Uri.parse(filePath)
|
||||
runCatching {
|
||||
DocumentFile.fromSingleUri(context.activity!!, fileUri)?.let { file ->
|
||||
if (!file.exists()) {
|
||||
context.shortToast(translation["file_not_found"])
|
||||
return@openFile
|
||||
}
|
||||
context.fileHandleManager.importFile(file.name!!) {
|
||||
context.androidContext.contentResolver.openInputStream(fileUri)?.use { inputStream ->
|
||||
inputStream.copyTo(this)
|
||||
}
|
||||
ImportFab {
|
||||
context.coroutineScope.launch {
|
||||
activityLauncherHelper.openFile { filePath ->
|
||||
val fileUri = Uri.parse(filePath)
|
||||
runCatching {
|
||||
DocumentFile.fromSingleUri(context.activity!!, fileUri)?.let { file ->
|
||||
if (!file.exists()) {
|
||||
context.shortToast(translation["file_not_found"])
|
||||
return@openFile
|
||||
}
|
||||
context.fileHandleManager.importFile(file.name!!) {
|
||||
context.androidContext.contentResolver.openInputStream(fileUri)?.use { inputStream ->
|
||||
inputStream.copyTo(this)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.error("Failed to import file", it)
|
||||
context.shortToast(translation.format("file_import_failed", "error" to it.message.toString()))
|
||||
}.onSuccess {
|
||||
context.shortToast(translation["file_imported"])
|
||||
coroutineScope.launch {
|
||||
reloadDispatcher.dispatch()
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.error("Failed to import file", it)
|
||||
context.shortToast(translation.format("file_import_failed", "error" to it.message.toString()))
|
||||
}.onSuccess {
|
||||
context.shortToast(translation["file_imported"])
|
||||
coroutineScope.launch {
|
||||
reloadDispatcher.dispatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,78 +142,25 @@ class FileImportsRoot: Routes.Route() {
|
||||
val files = rememberAsyncMutableStateList(defaultValue = listOf(), updateDispatcher = reloadDispatcher) {
|
||||
context.fileHandleManager.getStoredFiles()
|
||||
}
|
||||
val density = LocalDensity.current
|
||||
val titleText = context.translation["manager.routes.file_imports"]
|
||||
var topBarHeight by remember { mutableStateOf(0.dp) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
contentPadding = PaddingValues(
|
||||
top = topBarHeight + 8.dp,
|
||||
bottom = routes.bottomPadding + 16.dp
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = Color.Transparent,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 12.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay, RoundedCornerShape(24.dp))
|
||||
.padding(horizontal = 18.dp, vertical = 14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = PurrfectPalette.cardOverlayColor
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.AttachFile,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = translation["import_file_button"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Text(
|
||||
text = translation["manager.dialogs.file_imports.settings_select_file_hint"],
|
||||
color = PurrfectPalette.textSecondary,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
contentPadding = PaddingValues(bottom = routes.bottomPadding + 16.dp, top = 4.dp)
|
||||
) {
|
||||
item {
|
||||
if (files.isEmpty()) {
|
||||
Surface(
|
||||
@@ -279,7 +265,15 @@ class FileImportsRoot: Routes.Route() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingTopBar(
|
||||
title = titleText ?: translation["import_file_button"],
|
||||
subtitle = null,
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
modifier = Modifier.onGloballyPositioned {
|
||||
topBarHeight = with(density) { it.size.height.toDp() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,11 +385,12 @@ class LoggerHistoryRoot : Routes.Route() {
|
||||
}
|
||||
}
|
||||
|
||||
TextField(
|
||||
OutlinedTextField(
|
||||
value = stringFilter,
|
||||
onValueChange = { stringFilter = it },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = context.translation["manager.dialogs.add_friend.search_hint"] ?: "Search",
|
||||
|
||||
@@ -14,7 +14,12 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -29,6 +34,8 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
@@ -128,7 +135,8 @@ class ConfigExportSummaryScreen : Routes.Route() {
|
||||
}
|
||||
|
||||
override val content: @Composable (androidx.navigation.NavBackStackEntry) -> Unit = {
|
||||
val exportSensitiveData = it.arguments?.getBoolean("exportSensitiveData") ?: false
|
||||
val exportSensitiveData = it.arguments?.getString("exportSensitiveData")?.toBoolean() ?: false
|
||||
val exportLabel = context.translation["manager.sections.features.export_option"] ?: "Export"
|
||||
val parser = remember { ConfigParser() }
|
||||
val featuresByCategory = remember {
|
||||
parser.parse(context.config.exportToString(exportSensitiveData))
|
||||
@@ -140,11 +148,15 @@ class ConfigExportSummaryScreen : Routes.Route() {
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding()
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Bottom))
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = Color.Transparent,
|
||||
@@ -163,46 +175,36 @@ class ConfigExportSummaryScreen : Routes.Route() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay, RoundedCornerShape(24.dp))
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
IconButton(onClick = { routes.navController.popBackStack() }) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = translation["back_button_description"],
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = translation["title"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = PurrfectPalette.glowPrimary.copy(alpha = 0.22f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 10.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.45f)
|
||||
)
|
||||
)
|
||||
Button(
|
||||
onClick = { routes.navController.popBackStack() },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.28f),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
IconButton(
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
Text(context.translation["common.back"] ?: "Back")
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Summary",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
routes.activityLauncher.saveFile("config.json", "application/json") { uri ->
|
||||
runCatching {
|
||||
@@ -220,14 +222,19 @@ class ConfigExportSummaryScreen : Routes.Route() {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.32f),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDownward,
|
||||
contentDescription = translation["save_button"],
|
||||
tint = Color.White
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
}
|
||||
Text(exportLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,12 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -185,13 +190,18 @@ class ConfigImportConfirmationScreen : Routes.Route() {
|
||||
routes.configJsonForImport?.let { parser.parse(it) } ?: emptyMap()
|
||||
}
|
||||
val expandedState = remember { mutableStateMapOf<String, Boolean>() }
|
||||
val importLabel = translation["confirm_button"] ?: "Import"
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -213,29 +223,34 @@ class ConfigImportConfirmationScreen : Routes.Route() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay, RoundedCornerShape(24.dp))
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
Button(
|
||||
onClick = { routes.navController.popBackStack() },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.28f),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
IconButton(onClick = { routes.navController.popBackStack() }) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = translation["back_button_description"],
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = translation["title"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
Text(context.translation["common.back"] ?: "Back")
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Summary",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
@@ -261,7 +276,7 @@ class ConfigImportConfirmationScreen : Routes.Route() {
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(translation["confirm_button"])
|
||||
Text(importLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,118 +291,118 @@ class ConfigImportConfirmationScreen : Routes.Route() {
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
items(featuresByCategory.toList()) { (category, features) ->
|
||||
val isExpanded = expandedState[category] ?: false
|
||||
val rotationState by animateFloatAsState(targetValue = if (isExpanded) 180f else 0f)
|
||||
items(featuresByCategory.toList()) { (category, features) ->
|
||||
val isExpanded = expandedState[category] ?: false
|
||||
val rotationState by animateFloatAsState(targetValue = if (isExpanded) 180f else 0f)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { expandedState[category] = !isExpanded },
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = PurrfectPalette.cardOverlayColor,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 10.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.4f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.32f)
|
||||
)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { expandedState[category] = !isExpanded },
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = PurrfectPalette.cardOverlayColor,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 10.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.4f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.32f)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = category,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 17.sp,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { expandedState[category] = !isExpanded }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = translation["expand_button_description"],
|
||||
modifier = Modifier.graphicsLayer(rotationZ = rotationState),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = category,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 17.sp,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { expandedState[category] = !isExpanded }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = translation["expand_button_description"],
|
||||
modifier = Modifier.graphicsLayer(rotationZ = rotationState),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = isExpanded) {
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
features.forEachIndexed { index, feature ->
|
||||
when (val parsedValue = parser.parseValue(feature.key, feature.value)) {
|
||||
is List<*> -> {
|
||||
AnimatedVisibility(visible = isExpanded) {
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
features.forEachIndexed { index, feature ->
|
||||
when (val parsedValue = parser.parseValue(feature.key, feature.value)) {
|
||||
is List<*> -> {
|
||||
Column(
|
||||
modifier = Modifier.padding(start = (feature.indentation * 16).dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = feature.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(start = (feature.indentation * 16).dp),
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = feature.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
parsedValue.forEachIndexed { itemIndex, item ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
NumberBubble(itemIndex + 1)
|
||||
Text(
|
||||
text = item.toString(),
|
||||
color = PurrfectPalette.textSecondary
|
||||
)
|
||||
}
|
||||
parsedValue.forEachIndexed { itemIndex, item ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
NumberBubble(itemIndex + 1)
|
||||
Text(
|
||||
text = item.toString(),
|
||||
color = PurrfectPalette.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is String -> {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = (feature.indentation * 16).dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = feature.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = parsedValue,
|
||||
color = PurrfectPalette.glowSecondary,
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
is String -> {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = (feature.indentation * 16).dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = feature.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = parsedValue,
|
||||
color = PurrfectPalette.glowSecondary,
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (index < features.size - 1) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (index < features.size - 1) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NumberBubble(number: Int) {
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -400,6 +401,7 @@ class FeaturesRootSection : Routes.Route() {
|
||||
}
|
||||
|
||||
val propertyValue = property.value
|
||||
fun persistConfig() = context.config.writeConfig()
|
||||
|
||||
if (property.key.params.flags.contains(ConfigFlag.USER_IMPORT)) {
|
||||
registerDialogOnClickCallback()
|
||||
@@ -412,11 +414,13 @@ class FeaturesRootSection : Routes.Route() {
|
||||
isEmpty = it.isEmpty()
|
||||
if (isEmpty) {
|
||||
propertyValue.setAny(null)
|
||||
persistConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
var selectedFile by remember(files.size) { mutableStateOf(files.firstOrNull { it.name == propertyValue.getNullable() }.also {
|
||||
if (files.isNotEmpty() && it == null) propertyValue.setAny(null)
|
||||
if (files.isNotEmpty() && it == null) persistConfig()
|
||||
}?.name) }
|
||||
|
||||
Surface(
|
||||
@@ -487,6 +491,7 @@ class FeaturesRootSection : Routes.Route() {
|
||||
.clickable {
|
||||
selectedFile = if (isSelected) null else file.name
|
||||
propertyValue.setAny(selectedFile)
|
||||
persistConfig()
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = Color.White.copy(alpha = 0.05f),
|
||||
@@ -555,6 +560,7 @@ class FeaturesRootSection : Routes.Route() {
|
||||
activityLauncher {
|
||||
chooseFolder { uri ->
|
||||
propertyValue.setAny(uri)
|
||||
persistConfig()
|
||||
}
|
||||
}
|
||||
}.let { { it.invoke(true) } }) {
|
||||
@@ -575,6 +581,7 @@ class FeaturesRootSection : Routes.Route() {
|
||||
}
|
||||
state = state.not()
|
||||
propertyValue.setAny(state)
|
||||
persistConfig()
|
||||
},
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
@@ -722,6 +729,7 @@ class FeaturesRootSection : Routes.Route() {
|
||||
}
|
||||
state = state.not()
|
||||
container.globalState = state
|
||||
persistConfig()
|
||||
},
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
@@ -1518,6 +1526,7 @@ class FeaturesRootSection : Routes.Route() {
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
var controlsHeight by remember { mutableStateOf(96.dp) }
|
||||
val listState = rememberLazyListState()
|
||||
val sharedSearchHistory = remember { mutableStateListOf<String>().apply { addAll(loadSearchHistory()) } }
|
||||
var liveSearchQuery by rememberSaveable { mutableStateOf(searchKeyword.orEmpty()) }
|
||||
val isActiveSearch = isSearchResults || liveSearchQuery.isNotBlank()
|
||||
@@ -1544,12 +1553,18 @@ class FeaturesRootSection : Routes.Route() {
|
||||
liveSearchQuery = searchKeyword
|
||||
}
|
||||
}
|
||||
LaunchedEffect(liveSearchQuery) {
|
||||
if (!listState.isScrollInProgress) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
FeatureAuroraBackdrop()
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
contentPadding = PaddingValues(
|
||||
start = 6.dp,
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
package me.eternal.purrfectsnap.ui.manager.pages.home
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import me.eternal.purrfectsnap.R
|
||||
import me.eternal.purrfectsnap.common.util.ktx.openLink
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import android.os.SystemClock
|
||||
|
||||
class HomeAbout : Routes.Route() {
|
||||
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||
val avenirNext = remember {
|
||||
FontFamily(Font(R.font.avenir_next_medium, FontWeight.Medium))
|
||||
}
|
||||
val scrollState = rememberScrollState()
|
||||
val aboutStory = remember {
|
||||
"""
|
||||
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 <RSR/> joined the team, and this app soon became a huge success. We 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. We 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. Lastly, we would like to thank all our admins, notably: CLASSIC GENIUS, Harry, SUJΛL, Zain & scrodingerspet, 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.
|
||||
|
||||
|
||||
""".trimIndent()
|
||||
}
|
||||
val pagePadding = 16.dp
|
||||
val bottomPadding = routes.bottomPadding +
|
||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +
|
||||
24.dp
|
||||
val tapSource = remember { MutableInteractionSource() }
|
||||
val tapTimeoutMs = 1500L
|
||||
val tapCount = remember { mutableIntStateOf(0) }
|
||||
val lastTapTime = remember { mutableLongStateOf(0L) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(bottom = bottomPadding)
|
||||
) {
|
||||
FloatingTopBar(
|
||||
title = routeInfo.translatedKey?.value ?: "About",
|
||||
onBack = { routes.navController.popBackStack() }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = pagePadding)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(30.dp),
|
||||
color = Color.Transparent,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f)),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.panelGradient)
|
||||
.padding(horizontal = 22.dp, vertical = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "PurrfectSnap",
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
color = PurrfectPalette.textPrimary,
|
||||
fontFamily = avenirNext,
|
||||
modifier = Modifier.clickable(
|
||||
interactionSource = tapSource,
|
||||
indication = null
|
||||
) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
if (now - lastTapTime.longValue > tapTimeoutMs) {
|
||||
tapCount.intValue = 0
|
||||
}
|
||||
tapCount.intValue += 1
|
||||
lastTapTime.longValue = now
|
||||
if (tapCount.intValue >= 5) {
|
||||
tapCount.intValue = 0
|
||||
routes.retroGame.navigate()
|
||||
}
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = "An Xposed Module meant to enhance your Snapchat experience!",
|
||||
fontSize = 13.sp,
|
||||
color = PurrfectPalette.textSecondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "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 = "<RSR/>",
|
||||
imageRes = R.drawable.pfp_rsr,
|
||||
avenirNext = avenirNext,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = pagePadding)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(26.dp),
|
||||
color = Color.Transparent,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f)),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay)
|
||||
.padding(horizontal = 20.dp, vertical = 18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Our Story",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = aboutStory,
|
||||
fontSize = 14.sp,
|
||||
color = PurrfectPalette.textSecondary,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = pagePadding)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "With love, PurrfectSnap Team",
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { context.androidContext.openLink("https://github.com/particle-box/PurrfectSnap") },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color(0xFF1B152E)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_github),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = "GitHub", maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { context.androidContext.openLink("https://t.me/purrfectsnap_official") },
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.35f)),
|
||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_telegram),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = "Telegram", maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeveloperCard(
|
||||
name: String,
|
||||
imageRes: Int,
|
||||
avenirNext: FontFamily,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val cardShape = RoundedCornerShape(20.dp)
|
||||
val imageRing = Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary,
|
||||
PurrfectPalette.glowSecondary
|
||||
)
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = cardShape,
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(82.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White.copy(alpha = 0.1f))
|
||||
.border(2.dp, imageRing, CircleShape)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = imageRes),
|
||||
contentDescription = name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
fontFamily = avenirNext,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +226,12 @@ class HomeLogs : Routes.Route() {
|
||||
) {
|
||||
val firstVisibleItem by remember { derivedStateOf { logListState.firstVisibleItemIndex } }
|
||||
val layoutInfo by remember { derivedStateOf { logListState.layoutInfo } }
|
||||
val floatingButtonColors = IconButtonDefaults.filledIconButtonColors(
|
||||
containerColor = PurrfectPalette.cardOverlayColor,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = Color.White.copy(alpha = 0.08f),
|
||||
disabledContentColor = Color.White.copy(alpha = 0.35f)
|
||||
)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
@@ -243,7 +249,8 @@ class HomeLogs : Routes.Route() {
|
||||
logListState.scrollToItem(0)
|
||||
}
|
||||
},
|
||||
enabled = firstVisibleItem != 0
|
||||
enabled = firstVisibleItem != 0,
|
||||
colors = floatingButtonColors
|
||||
) {
|
||||
Icon(Icons.Filled.KeyboardDoubleArrowUp, contentDescription = null)
|
||||
}
|
||||
@@ -253,7 +260,8 @@ class HomeLogs : Routes.Route() {
|
||||
logListState.scrollToItem((logListState.layoutInfo.totalItemsCount - 1).takeIf { it >= 0 } ?: return@launch)
|
||||
}
|
||||
},
|
||||
enabled = layoutInfo.visibleItemsInfo.lastOrNull()?.index != layoutInfo.totalItemsCount - 1
|
||||
enabled = layoutInfo.visibleItemsInfo.lastOrNull()?.index != layoutInfo.totalItemsCount - 1,
|
||||
colors = floatingButtonColors
|
||||
) {
|
||||
Icon(Icons.Filled.KeyboardDoubleArrowDown, contentDescription = null)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package me.eternal.purrfectsnap.ui.manager.pages.home
|
||||
package me.eternal.purrfectsnap.ui.manager.pages.home
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
@@ -6,11 +6,8 @@ import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Canvas
|
||||
@@ -19,7 +16,6 @@ import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -31,19 +27,24 @@ import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -55,18 +56,21 @@ import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.DragHandle
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.outlined.Widgets
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
@@ -77,7 +81,6 @@ import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -91,9 +94,7 @@ import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
@@ -105,14 +106,14 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.eternal.purrfectsnap.R
|
||||
import me.eternal.purrfectsnap.action.EnumQuickActions
|
||||
import me.eternal.purrfectsnap.common.BuildConfig
|
||||
@@ -126,9 +127,13 @@ import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
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.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.util.ActivityLauncherHelper
|
||||
import me.eternal.purrfectsnap.ui.util.AlertDialogs
|
||||
import me.eternal.purrfectsnap.ui.util.scaleOnPress
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
class HomeRootSection : Routes.Route() {
|
||||
override val translation by lazy { context.translation.getCategory("manager.sections.home") }
|
||||
@@ -144,6 +149,10 @@ class HomeRootSection : Routes.Route() {
|
||||
)
|
||||
}
|
||||
|
||||
private val changelogClient by lazy { OkHttpClient() }
|
||||
private val changelogStableUrl = "https://raw.githubusercontent.com/particle-box/PurrfectSnap/dev/changelogs-stable.txt"
|
||||
private val changelogPrereleaseUrl = "https://raw.githubusercontent.com/particle-box/PurrfectSnap/dev/changelogs-prerelease.txt"
|
||||
|
||||
private val heroGradientColors = listOf(
|
||||
Color(0xFF5C4B99),
|
||||
Color(0xFF322B5E),
|
||||
@@ -278,6 +287,26 @@ class HomeRootSection : Routes.Route() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoCard(content: @Composable ColumnScope.() -> Unit) {
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.padding(start = cardMargin, end = cardMargin)
|
||||
.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = 10.dp),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.HomeActionChips() {
|
||||
TopBarActionChip(
|
||||
@@ -373,6 +402,7 @@ class HomeRootSection : Routes.Route() {
|
||||
downloadState: UpdateDownloader.DownloadState,
|
||||
downloadProgress: Float,
|
||||
onUpdateAction: () -> Unit,
|
||||
channelLabel: String,
|
||||
isPurrAuraActive: Boolean,
|
||||
onWikiClick: () -> Unit,
|
||||
onTelegramClick: () -> Unit,
|
||||
@@ -382,6 +412,7 @@ class HomeRootSection : Routes.Route() {
|
||||
avenirNext: FontFamily
|
||||
) {
|
||||
val heroShape = RoundedCornerShape(36.dp)
|
||||
val gitHashShort = remember { (context.installationSummary.modInfo?.gitHash ?: BuildConfig.GIT_HASH).take(7) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = cardMargin, vertical = 6.dp)
|
||||
@@ -412,9 +443,10 @@ class HomeRootSection : Routes.Route() {
|
||||
fontFamily = avenirNext
|
||||
)
|
||||
Text(
|
||||
text = "by $authorName",
|
||||
text = "By ΞTΞRNAL",
|
||||
color = Color.White.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp
|
||||
fontSize = 14.sp,
|
||||
fontFamily = avenirNext
|
||||
)
|
||||
Text(
|
||||
text = "An Xposed Module meant to enhance your Snapchat experience",
|
||||
@@ -429,10 +461,119 @@ class HomeRootSection : Routes.Route() {
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
HeroBadge("Codename: Rass Malayi")
|
||||
HeroBadge("Debug Build")
|
||||
HeroBadge("Version: $versionName - $channelLabel")
|
||||
gitHashShort.takeIf { it.isNotBlank() && it.lowercase() != "unknown" }?.let {
|
||||
HeroBadge("Build: $it")
|
||||
}
|
||||
}
|
||||
|
||||
if (latestUpdate != null) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.14f)),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = translation["update_title"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = translation.format(
|
||||
"update_content",
|
||||
"version" to (latestUpdate.versionName)
|
||||
),
|
||||
color = Color.White.copy(alpha = 0.82f),
|
||||
fontSize = 12.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = downloadState,
|
||||
label = "UpdateDownloadHero"
|
||||
) { state ->
|
||||
when (state) {
|
||||
UpdateDownloader.DownloadState.IDLE,
|
||||
UpdateDownloader.DownloadState.FAILED -> {
|
||||
Button(
|
||||
onClick = onUpdateAction,
|
||||
shape = RoundedCornerShape(50),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color(0xFF1B152E)
|
||||
),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)),
|
||||
contentPadding = PaddingValues(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Download,
|
||||
contentDescription = translation["download_icon_description"],
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
UpdateDownloader.DownloadState.DOWNLOADING -> {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
progress = { downloadProgress },
|
||||
modifier = Modifier.size(28.dp),
|
||||
strokeWidth = 3.dp,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = "${(downloadProgress * 100).toInt()}%",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
UpdateDownloader.DownloadState.COMPLETED -> {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = translation["completed_icon_description"],
|
||||
tint = Color(0xFFA3F0C2)
|
||||
)
|
||||
Text(
|
||||
text = "Ready to install",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
@@ -564,23 +705,6 @@ class HomeRootSection : Routes.Route() {
|
||||
prefs.edit().remove("quick_tile_size_$key").apply()
|
||||
}
|
||||
|
||||
private fun getTileOffset(name: String): Pair<Float, Float> {
|
||||
val prefs = context.sharedPreferences
|
||||
val key = resolveTileKey(name)
|
||||
val raw = prefs.getString("quick_tile_offset_$key", null)
|
||||
if (raw == null) return 0f to 0f
|
||||
val parts = raw.split(',')
|
||||
val x = parts.getOrNull(0)?.toFloatOrNull() ?: 0f
|
||||
val y = parts.getOrNull(1)?.toFloatOrNull() ?: 0f
|
||||
return x to y
|
||||
}
|
||||
|
||||
private fun setTileOffset(name: String, x: Float, y: Float) {
|
||||
val prefs = context.sharedPreferences
|
||||
val key = resolveTileKey(name)
|
||||
prefs.edit().putString("quick_tile_offset_$key", "$x,$y").apply()
|
||||
}
|
||||
|
||||
private fun clearTileOffset(name: String) {
|
||||
val prefs = context.sharedPreferences
|
||||
val key = resolveTileKey(name)
|
||||
@@ -611,11 +735,22 @@ class HomeRootSection : Routes.Route() {
|
||||
val selectedTiles = rememberAsyncMutableStateList(defaultValue = listOf()) {
|
||||
context.database.getQuickTiles().filter { it.isNotBlank() }
|
||||
}
|
||||
val latestUpdate by rememberAsyncMutableState(defaultValue = null) { Updater.latestRelease }
|
||||
val updateChannel = context.config.root.global.updateSettings.updateChannel.getNullable() ?: "stable"
|
||||
val channelLabel = if (updateChannel == "prerelease") "Pre-release" else "Stable"
|
||||
val latestUpdate by rememberAsyncMutableState(defaultValue = null, keys = arrayOf(updateChannel)) {
|
||||
val channel = if (updateChannel == "prerelease") Channel.PRERELEASE else Channel.STABLE
|
||||
Updater.getLatestRelease(channel)
|
||||
}
|
||||
val changelogUrl = if (updateChannel == "prerelease") changelogPrereleaseUrl else changelogStableUrl
|
||||
val downloadState by UpdateDownloader.downloadState.collectAsState()
|
||||
val downloadProgress by UpdateDownloader.downloadProgress.collectAsState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val isPurrAuraActive by rememberPreferenceBool("debug_test_mode", true)
|
||||
var showChangelogDialog by remember { mutableStateOf(false) }
|
||||
var changelogLoading by remember { mutableStateOf(false) }
|
||||
var changelogError by remember { mutableStateOf<String?>(null) }
|
||||
var changelogText by remember { mutableStateOf<String?>(null) }
|
||||
var changelogVersion by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val handleUpdateAction: () -> Unit = {
|
||||
latestUpdate?.let { latest ->
|
||||
@@ -624,7 +759,7 @@ class HomeRootSection : Routes.Route() {
|
||||
for (abi in supportedAbis) {
|
||||
when (abi) {
|
||||
"arm64-v8a" -> {
|
||||
abiName = "armv8"
|
||||
abiName = "arm64"
|
||||
break
|
||||
}
|
||||
"armeabi-v7a" -> {
|
||||
@@ -633,29 +768,79 @@ class HomeRootSection : Routes.Route() {
|
||||
}
|
||||
}
|
||||
}
|
||||
context.log.info(
|
||||
"Update request: device ABIs=${supportedAbis.joinToString()} resolvedArch=${abiName ?: "unknown"}",
|
||||
"HomeRoot"
|
||||
)
|
||||
|
||||
if (latest.workflowId == null) {
|
||||
context.androidContext.openLink(latest.releaseUrl)
|
||||
} else if (abiName == null) {
|
||||
android.widget.Toast.makeText(
|
||||
context.androidContext,
|
||||
"Your device architecture is not supported for automatic updates.",
|
||||
android.widget.Toast.LENGTH_LONG
|
||||
).show()
|
||||
if (latest.workflowId != null) {
|
||||
if (abiName == null) {
|
||||
android.widget.Toast.makeText(
|
||||
context.androidContext,
|
||||
"Your device architecture is not supported for automatic updates.",
|
||||
android.widget.Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
val artifactName = "purrfectsnap-${abiName}-debug"
|
||||
val downloadUrl = "https://nightly.link/particle-box/PurrfectSnap/actions/runs/${latest.workflowId}/$artifactName.zip"
|
||||
context.log.info("Debug update -> downloading $artifactName from $downloadUrl", "HomeRoot")
|
||||
UpdateDownloader.downloadAndInstall(context, downloadUrl, "$artifactName.zip", coroutineScope)
|
||||
}
|
||||
return@let
|
||||
}
|
||||
|
||||
val releaseDownload = abiName?.let { arch -> latest.assetDownloads[arch] }
|
||||
if (releaseDownload != null) {
|
||||
val fileName = releaseDownload.substringAfterLast('/')
|
||||
context.log.info("Release update -> arch=$abiName url=$releaseDownload file=$fileName", "HomeRoot")
|
||||
UpdateDownloader.downloadAndInstall(context, releaseDownload, fileName, coroutineScope)
|
||||
} else {
|
||||
val artifactName = "purrfectsnap-${abiName}-debug"
|
||||
val downloadUrl = "https://nightly.link/particle-box/PurrfectSnap/actions/runs/${latest.workflowId}/$artifactName.zip"
|
||||
UpdateDownloader.downloadAndInstall(context.androidContext, downloadUrl, "$artifactName.zip", coroutineScope)
|
||||
context.log.warn(
|
||||
"No matching update asset for arch=$abiName (available: ${latest.assetDownloads.keys})",
|
||||
"HomeRoot"
|
||||
)
|
||||
context.androidContext.openLink(latest.releaseUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadChangelog(targetVersion: String, url: String) {
|
||||
if (changelogVersion == targetVersion && changelogText != null) return
|
||||
changelogLoading = true
|
||||
changelogError = null
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
changelogClient.newCall(Request.Builder().url(url).build()).execute().use { response ->
|
||||
if (!response.isSuccessful) throw IllegalStateException("Failed to fetch changelog (${response.code})")
|
||||
val body = response.body?.string() ?: throw IllegalStateException("Empty changelog body")
|
||||
extractChangelogForVersion(body, targetVersion).ifBlank { body.trim() }
|
||||
}
|
||||
}.onSuccess { text ->
|
||||
withContext(Dispatchers.Main) {
|
||||
changelogText = text
|
||||
changelogVersion = targetVersion
|
||||
changelogLoading = false
|
||||
}
|
||||
}.onFailure { error ->
|
||||
withContext(Dispatchers.Main) {
|
||||
changelogError = error.message ?: "Failed to load changelog"
|
||||
changelogLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onUpdateButtonClick: () -> Unit = {
|
||||
latestUpdate?.let {
|
||||
showChangelogDialog = true
|
||||
loadChangelog(it.versionName, changelogUrl)
|
||||
}
|
||||
}
|
||||
|
||||
var showQuickActionsMenu by remember { mutableStateOf(false) }
|
||||
var editMode by remember { mutableStateOf(false) }
|
||||
val scrollState = rememberScrollState()
|
||||
val statusBarPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
val density = LocalDensity.current
|
||||
val contentBottomPadding = routes.bottomPadding + navigationBarPadding + 96.dp
|
||||
|
||||
Box(
|
||||
@@ -666,7 +851,7 @@ class HomeRootSection : Routes.Route() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState, enabled = !editMode)
|
||||
.verticalScroll(scrollState)
|
||||
.padding(bottom = contentBottomPadding)
|
||||
) {
|
||||
Row(
|
||||
@@ -680,18 +865,19 @@ class HomeRootSection : Routes.Route() {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
HomeActionChips()
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
HeroSection(
|
||||
versionName = BuildConfig.VERSION_NAME,
|
||||
latestUpdate = latestUpdate,
|
||||
downloadState = downloadState,
|
||||
downloadProgress = downloadProgress,
|
||||
onUpdateAction = handleUpdateAction,
|
||||
onUpdateAction = onUpdateButtonClick,
|
||||
channelLabel = channelLabel,
|
||||
isPurrAuraActive = isPurrAuraActive,
|
||||
onWikiClick = { context.androidContext.openLink("https://github.com/particle-box/PurrfectSnap/wiki") },
|
||||
onTelegramClick = { context.androidContext.openLink("https://t.me/purrfectsnap_official") },
|
||||
onGithubClick = { context.androidContext.openLink("https://github.com/particle-box/PurrfectSnap") },
|
||||
authorName = "ΞTΞRNAL",
|
||||
authorName = "ETERNAL",
|
||||
onManageClick = { routes.settings.navigate() },
|
||||
avenirNext = avenirNext,
|
||||
)
|
||||
@@ -723,7 +909,8 @@ class HomeRootSection : Routes.Route() {
|
||||
color = Color.White.copy(alpha = 0.85f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Column(
|
||||
@@ -809,184 +996,27 @@ class HomeRootSection : Routes.Route() {
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(text = "Manage")
|
||||
}
|
||||
Button(
|
||||
onClick = { editMode = !editMode },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (editMode) Color.White else Color.White.copy(alpha = 0.12f),
|
||||
contentColor = if (editMode) Color(0xFF1B152E) else Color.White
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (editMode) Icons.Default.Check else Icons.Filled.DragHandle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(if (editMode) "Done" else "Reorder")
|
||||
}
|
||||
}
|
||||
}
|
||||
val spacing = 12.dp
|
||||
var spanTick by remember { mutableIntStateOf(0) }
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val baseCell = remember { (maxWidth - (spacing * 2)) / 3f }
|
||||
val baseCellPx = with(density) { baseCell.toPx() }
|
||||
|
||||
val (tilePositions, totalHeight) = remember(selectedTiles.size, spanTick) {
|
||||
val positions = mutableMapOf<String, Offset>()
|
||||
var currentX = 0f
|
||||
var currentY = 0f
|
||||
var rowMaxHeight = 0f
|
||||
val screenWidthPx = with(density) { maxWidth.toPx() }
|
||||
|
||||
selectedTiles.forEach { tileName ->
|
||||
cards.entries.find { entry -> entry.key.first == tileName }?.let {
|
||||
val card = it.key
|
||||
val (wSpan, hSpan) = getTileSpan(card.first)
|
||||
val tileWidthPx = with(density) { (baseCell * wSpan + spacing * (wSpan - 1)).toPx() }
|
||||
val tileHeightPx = with(density) { (baseCell * hSpan + spacing * (hSpan - 1)).toPx() }
|
||||
|
||||
if (currentX + tileWidthPx > screenWidthPx) {
|
||||
currentX = 0f
|
||||
currentY += rowMaxHeight
|
||||
rowMaxHeight = 0f
|
||||
}
|
||||
|
||||
positions[tileName] = Offset(currentX, currentY)
|
||||
currentX += tileWidthPx + with(density) { spacing.toPx() }
|
||||
if (tileHeightPx > rowMaxHeight) {
|
||||
rowMaxHeight = tileHeightPx
|
||||
}
|
||||
}
|
||||
}
|
||||
positions to (currentY + rowMaxHeight)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.height(with(density) { totalHeight.toDp() })) {
|
||||
remember(selectedTiles.size, context.translation.loadedLocale) {
|
||||
selectedTiles.mapNotNull {
|
||||
cards.entries.find { entry -> entry.key.first == it }
|
||||
}
|
||||
}.forEach { (card, action) ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val _tick = spanTick
|
||||
val (wSpan, hSpan) = getTileSpan(card.first)
|
||||
val tileWidth = baseCell * wSpan + spacing * (wSpan - 1)
|
||||
val tileHeight = baseCell * hSpan + spacing * (hSpan - 1)
|
||||
val tileWidthPx = with(density) { tileWidth.toPx() }
|
||||
val tileHeightPx = with(density) { tileHeight.toPx() }
|
||||
|
||||
var offsetX by remember(card.first) { mutableStateOf(0f) }
|
||||
var offsetY by remember(card.first) { mutableStateOf(0f) }
|
||||
var isDragging by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(card.first, spanTick) {
|
||||
val (x, y) = getTileOffset(card.first)
|
||||
if (x != 0f || y != 0f) {
|
||||
offsetX = x
|
||||
offsetY = y
|
||||
} else {
|
||||
val pos = tilePositions[card.first]
|
||||
if (pos != null) {
|
||||
offsetX = pos.x
|
||||
offsetY = pos.y
|
||||
setTileOffset(card.first, offsetX, offsetY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val animatedOffsetX by animateFloatAsState(
|
||||
targetValue = offsetX,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
),
|
||||
label = "offsetX"
|
||||
)
|
||||
val animatedOffsetY by animateFloatAsState(
|
||||
targetValue = offsetY,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
),
|
||||
label = "offsetY"
|
||||
)
|
||||
|
||||
val currentOffsetX = if (isDragging) offsetX else animatedOffsetX
|
||||
val currentOffsetY = if (isDragging) offsetY else animatedOffsetY
|
||||
|
||||
val baseModifier = Modifier
|
||||
.offset { IntOffset(currentOffsetX.roundToInt(), currentOffsetY.roundToInt()) }
|
||||
.width(tileWidth)
|
||||
.height(tileHeight)
|
||||
.padding(all = 6.dp)
|
||||
|
||||
val editModifier = baseModifier.then(
|
||||
Modifier.pointerInput(card.first, tileWidthPx, tileHeightPx) {
|
||||
var originalOffsetX = 0f
|
||||
var originalOffsetY = 0f
|
||||
detectDragGestures(
|
||||
onDragStart = {
|
||||
isDragging = true
|
||||
originalOffsetX = offsetX
|
||||
originalOffsetY = offsetY
|
||||
},
|
||||
onDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
offsetX += dragAmount.x
|
||||
offsetY += dragAmount.y
|
||||
},
|
||||
onDragEnd = {
|
||||
isDragging = false
|
||||
var targetTile: String? = null
|
||||
var maxOverlap = 0f
|
||||
val tileRect = Rect(Offset(offsetX, offsetY), Size(tileWidthPx, tileHeightPx))
|
||||
|
||||
for (otherTileName in selectedTiles) {
|
||||
if (otherTileName == card.first) continue
|
||||
val (otherOffsetX, otherOffsetY) = getTileOffset(otherTileName)
|
||||
val (otherWSpan, otherHSpan) = getTileSpan(otherTileName)
|
||||
val otherTileWidth = baseCell * otherWSpan + spacing * (otherWSpan - 1)
|
||||
val otherTileHeight = baseCell * otherHSpan + spacing * (otherHSpan - 1)
|
||||
val otherRect = Rect(Offset(otherOffsetX, otherOffsetY), Size(with(density) { otherTileWidth.toPx() }, with(density) { otherTileHeight.toPx() }))
|
||||
val intersectRect = tileRect.intersect(otherRect)
|
||||
val overlapArea = intersectRect.width * intersectRect.height
|
||||
if (overlapArea > maxOverlap) {
|
||||
maxOverlap = overlapArea
|
||||
targetTile = otherTileName
|
||||
}
|
||||
}
|
||||
|
||||
if (targetTile != null) {
|
||||
val (wSpan, hSpan) = getTileSpan(card.first)
|
||||
val (targetWSpan, targetHSpan) = getTileSpan(targetTile!!)
|
||||
if (wSpan == targetWSpan && hSpan == targetHSpan) {
|
||||
// swap
|
||||
val (targetOffsetX, targetOffsetY) = getTileOffset(targetTile!!)
|
||||
setTileOffset(card.first, targetOffsetX, targetOffsetY)
|
||||
setTileOffset(targetTile!!, originalOffsetX, originalOffsetY)
|
||||
spanTick++ // this will trigger recomposition for all tiles
|
||||
} else {
|
||||
// revert
|
||||
offsetX = originalOffsetX
|
||||
offsetY = originalOffsetY
|
||||
}
|
||||
} else {
|
||||
setTileOffset(card.first, offsetX, offsetY)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
val viewModifier = baseModifier.then(Modifier.scaleOnPress(interactionSource))
|
||||
|
||||
val tileContent: @Composable (Modifier, Boolean) -> Unit = { tileModifier, isEdit ->
|
||||
val spacing = 12.dp
|
||||
val gridPadding = 8.dp
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 120.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing),
|
||||
verticalArrangement = Arrangement.spacedBy(spacing),
|
||||
contentPadding = PaddingValues(gridPadding)
|
||||
) {
|
||||
items(selectedTiles, key = { it }) { tileName ->
|
||||
val cardEntry = cards.entries.find { entry -> entry.key.first == tileName } ?: return@items
|
||||
val (card, action) = cardEntry
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Surface(
|
||||
modifier = tileModifier,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1.05f)
|
||||
.scaleOnPress(interactionSource)
|
||||
.clickable { action(routes) },
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = Color.White.copy(alpha = 0.06f),
|
||||
tonalElevation = 0.dp,
|
||||
@@ -1011,86 +1041,29 @@ class HomeRootSection : Routes.Route() {
|
||||
.fillMaxSize()
|
||||
.padding(all = 10.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = card.second, contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(50.dp)
|
||||
modifier = Modifier.size(44.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = card.first,
|
||||
lineHeight = 16.sp,
|
||||
fontSize = 14.sp,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.White,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
if (isEdit) {
|
||||
var dxAccResize by remember(card.first, spanTick) { mutableStateOf(0f) }
|
||||
var dyAccResize by remember(card.first, spanTick) { mutableStateOf(0f) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(28.dp)
|
||||
.pointerInput(card.first, spanTick) {
|
||||
detectDragGestures(
|
||||
onDragStart = {
|
||||
dxAccResize = 0f
|
||||
dyAccResize = 0f
|
||||
},
|
||||
onDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
dxAccResize += dragAmount.x
|
||||
dyAccResize += dragAmount.y
|
||||
|
||||
var newW = wSpan
|
||||
var newH = hSpan
|
||||
val step = baseCellPx / 2f
|
||||
|
||||
while (dxAccResize > step) {
|
||||
newW = (wSpan + 1).coerceIn(1, 3)
|
||||
dxAccResize -= step
|
||||
}
|
||||
while (dxAccResize < -step) {
|
||||
newW = (wSpan - 1).coerceIn(1, 3)
|
||||
dxAccResize += step
|
||||
}
|
||||
while (dyAccResize > step) {
|
||||
newH = (hSpan + 1).coerceIn(1, 3)
|
||||
dyAccResize -= step
|
||||
}
|
||||
while (dyAccResize < -step) {
|
||||
newH = (hSpan - 1).coerceIn(1, 3)
|
||||
dyAccResize += step
|
||||
}
|
||||
|
||||
if (newW != wSpan || newH != hSpan) {
|
||||
setTileSpan(card.first, newW, newH)
|
||||
spanTick++
|
||||
selectedTiles.forEach { clearTileOffset(it) }
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(Icons.Filled.DragHandle, contentDescription = null, tint = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (editMode) {
|
||||
tileContent(editModifier, true)
|
||||
} else {
|
||||
tileContent(viewModifier.then(Modifier.clickable { action(routes) }), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1098,6 +1071,71 @@ class HomeRootSection : Routes.Route() {
|
||||
}
|
||||
}
|
||||
|
||||
if (showChangelogDialog && latestUpdate != null) {
|
||||
AestheticDialog(
|
||||
onDismissRequest = { showChangelogDialog = false },
|
||||
title = "Changelog",
|
||||
text = "",
|
||||
icon = Icons.Filled.Info,
|
||||
confirmButtonText = "Update",
|
||||
onConfirm = {
|
||||
showChangelogDialog = false
|
||||
handleUpdateAction()
|
||||
},
|
||||
dismissButtonText = "Cancel",
|
||||
onDismiss = { showChangelogDialog = false },
|
||||
confirmEnabled = !changelogLoading,
|
||||
customContent = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 120.dp, max = 340.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
when {
|
||||
changelogLoading -> {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(28.dp),
|
||||
strokeWidth = 3.dp,
|
||||
color = Color.White
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = "Loading changelog…",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
changelogError != null -> {
|
||||
Text(
|
||||
text = changelogError ?: "Failed to load changelog",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Text(
|
||||
text = changelogText ?: "Changelog not available",
|
||||
color = PurrfectPalette.textPrimary,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showQuickActionsMenu) {
|
||||
QuickActionsDialog(
|
||||
quickActions = cards,
|
||||
@@ -1110,9 +1148,6 @@ class HomeRootSection : Routes.Route() {
|
||||
newList.forEach { clearTileOffset(it) }
|
||||
selectedTiles.clear()
|
||||
selectedTiles.addAll(newList)
|
||||
if (newList.isEmpty()) {
|
||||
editMode = false
|
||||
}
|
||||
context.coroutineScope.launch {
|
||||
context.database.setQuickTiles(selectedTiles)
|
||||
}
|
||||
@@ -1123,5 +1158,21 @@ class HomeRootSection : Routes.Route() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractChangelogForVersion(raw: String, version: String): String {
|
||||
val lines = raw.lines()
|
||||
val headerRegex = Regex("^\\s*#+\\s*v?${Regex.escape(version)}\\b", RegexOption.IGNORE_CASE)
|
||||
var collecting = false
|
||||
val collected = mutableListOf<String>()
|
||||
lines.forEach { line ->
|
||||
if (headerRegex.containsMatchIn(line)) {
|
||||
collecting = true
|
||||
return@forEach
|
||||
}
|
||||
if (collecting && line.startsWith("#")) return@forEach
|
||||
if (collecting) collected.add(line)
|
||||
}
|
||||
return collected.joinToString("\n").trim()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -20,6 +21,7 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
@@ -68,8 +70,28 @@ class HomeSettings : Routes.Route() {
|
||||
|
||||
private fun scheduleUpdateCheck() {
|
||||
val workManager = WorkManager.getInstance(context.androidContext)
|
||||
if (context.config.root.global.updateSettings.autoUpdateCheck.get()) {
|
||||
val frequency = context.config.root.global.updateSettings.updateCheckFrequency.get()
|
||||
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()
|
||||
}
|
||||
|
||||
if (autoUpdateCheck) {
|
||||
val repeatInterval = when (frequency) {
|
||||
"daily" -> 1L
|
||||
"weekly" -> 7L
|
||||
@@ -86,6 +108,7 @@ 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)
|
||||
.build()
|
||||
|
||||
val workRequest = PeriodicWorkRequestBuilder<UpdateCheckWorker>(repeatInterval, TimeUnit.DAYS)
|
||||
@@ -255,7 +278,7 @@ class HomeSettings : Routes.Route() {
|
||||
) { content(this) }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||
val contextC = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -291,6 +314,29 @@ class HomeSettings : Routes.Route() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AestheticDropdownField(
|
||||
value: String,
|
||||
expanded: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val shape = RoundedCornerShape(16.dp)
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.background(Color.White.copy(alpha = 0.06f))
|
||||
.border(1.dp, Color.White.copy(alpha = 0.16f), shape)
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = value, color = Color.White)
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
||||
}
|
||||
}
|
||||
|
||||
val topPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -374,29 +420,55 @@ class HomeSettings : Routes.Route() {
|
||||
GlassCard {
|
||||
RowTitle(title = translation["ui_settings_title"])
|
||||
ShiftedRow {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 55.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = translation["haptic_feedback_label"])
|
||||
var hapticFeedbackEnabled by remember { mutableStateOf(context.config.root.global.uiSettings.hapticFeedback.getNullable() ?: true) }
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Switch(
|
||||
checked = hapticFeedbackEnabled,
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
hapticFeedbackEnabled = it
|
||||
context.config.root.global.uiSettings.hapticFeedback.set(it)
|
||||
context.config.writeConfig()
|
||||
},
|
||||
modifier = Modifier.padding(end = 26.dp),
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 55.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = translation["haptic_feedback_label"])
|
||||
var hapticFeedbackEnabled by remember { mutableStateOf(context.config.root.global.uiSettings.hapticFeedback.getNullable() ?: true) }
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Switch(
|
||||
checked = hapticFeedbackEnabled,
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
hapticFeedbackEnabled = it
|
||||
context.config.root.global.uiSettings.hapticFeedback.set(it)
|
||||
context.config.writeConfig()
|
||||
},
|
||||
modifier = Modifier.padding(end = 26.dp),
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 55.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = translation["use_system_toasts_label"])
|
||||
var useSystemToasts by remember { mutableStateOf(context.config.root.global.uiSettings.useSystemToasts.getNullable() ?: false) }
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Switch(
|
||||
checked = useSystemToasts,
|
||||
onCheckedChange = {
|
||||
if (context.config.root.global.uiSettings.hapticFeedback.get()) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
useSystemToasts = it
|
||||
context.config.root.global.uiSettings.useSystemToasts.set(it)
|
||||
context.config.writeConfig()
|
||||
},
|
||||
modifier = Modifier.padding(end = 26.dp),
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,77 +477,115 @@ class HomeSettings : Routes.Route() {
|
||||
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 selectedFrequency by remember { mutableStateOf(context.config.root.global.updateSettings.updateCheckFrequency.getNullable() ?: "weekly") }
|
||||
var frequencyMenuExpanded by remember { mutableStateOf(false) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 55.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = translation["auto_update_check"])
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Switch(
|
||||
checked = autoUpdateCheck,
|
||||
onCheckedChange = {
|
||||
if (context.config.root.global.uiSettings.hapticFeedback.get()) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
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"])
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Switch(
|
||||
checked = autoUpdateCheck,
|
||||
onCheckedChange = {
|
||||
if (context.config.root.global.uiSettings.hapticFeedback.get()) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
autoUpdateCheck = it
|
||||
if (it && context.config.root.global.updateSettings.updateCheckFrequency.getNullable() == null) {
|
||||
selectedFrequency = "weekly"
|
||||
context.config.root.global.updateSettings.updateCheckFrequency.set("weekly")
|
||||
}
|
||||
context.config.root.global.updateSettings.autoUpdateCheck.set(it)
|
||||
context.config.writeConfig()
|
||||
scheduleUpdateCheck()
|
||||
},
|
||||
modifier = Modifier.padding(end = 26.dp),
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(visible = autoUpdateCheck) {
|
||||
val spacingModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 26.dp)
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = frequencyMenuExpanded,
|
||||
onExpandedChange = { frequencyMenuExpanded = it },
|
||||
expanded = channelMenuExpanded,
|
||||
onExpandedChange = { channelMenuExpanded = it },
|
||||
modifier = spacingModifier
|
||||
) {
|
||||
AestheticDropdownField(
|
||||
value = translation.getOrNull("update_channel_${selectedChannel}") ?: selectedChannel,
|
||||
expanded = channelMenuExpanded,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 26.dp)
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
||||
onClick = { channelMenuExpanded = true }
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = channelMenuExpanded,
|
||||
onDismissRequest = { channelMenuExpanded = false }
|
||||
) {
|
||||
TextField(
|
||||
value = translation.getOrNull("update_check_frequency_${selectedFrequency}") ?: selectedFrequency,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = frequencyMenuExpanded) },
|
||||
colors = ExposedDropdownMenuDefaults.textFieldColors(
|
||||
focusedContainerColor = Color.White.copy(alpha = 0.08f),
|
||||
unfocusedContainerColor = Color.White.copy(alpha = 0.06f),
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
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()
|
||||
}
|
||||
)
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = frequencyMenuExpanded,
|
||||
onDismissRequest = { frequencyMenuExpanded = false }
|
||||
) {
|
||||
listOf("daily", "weekly", "monthly").forEach { frequency ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = translation.getOrNull("update_check_frequency_${frequency}") ?: frequency) },
|
||||
onClick = {
|
||||
selectedFrequency = frequency
|
||||
frequencyMenuExpanded = false
|
||||
context.config.root.global.updateSettings.updateCheckFrequency.set(frequency)
|
||||
context.config.writeConfig()
|
||||
scheduleUpdateCheck()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GlassCard {
|
||||
RowTitle(title = "Reset PurrfectSnap")
|
||||
ShiftedRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 55.dp)
|
||||
.clickable {
|
||||
// Clear setup progress and route back to SetupActivity
|
||||
context.sharedPreferences.edit()
|
||||
.remove("setup_in_progress")
|
||||
.remove("setup_current_route")
|
||||
.remove("setup_skip_patch")
|
||||
.remove("setup_install_mode")
|
||||
.apply()
|
||||
|
||||
// Clear config to defaults
|
||||
context.config.reset()
|
||||
context.config.writeConfig()
|
||||
|
||||
// Launch setup activity fresh
|
||||
val intent = android.content.Intent(context.androidContext, me.eternal.purrfectsnap.ui.setup.SetupActivity::class.java)
|
||||
intent.flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.androidContext.startActivity(intent)
|
||||
|
||||
// Close current manager activity
|
||||
routes.navController.popBackStack()
|
||||
},
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Reset and restart setup",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
|
||||
contentDescription = "Reset",
|
||||
modifier = Modifier.padding(end = 14.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,6 +598,7 @@ class HomeSettings : Routes.Route() {
|
||||
var storedStoriesCount by rememberAsyncMutableState(defaultValue = 0) {
|
||||
context.messageLogger.getStoredStoriesCount()
|
||||
}
|
||||
var showImportDialog by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -507,12 +618,12 @@ class HomeSettings : Routes.Route() {
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Row(
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterHorizontally),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
@@ -576,6 +687,13 @@ class HomeSettings : Routes.Route() {
|
||||
) {
|
||||
Text(text = translation["clear_button"])
|
||||
}
|
||||
Button(
|
||||
onClick = { showImportDialog = true },
|
||||
colors = sharedButtonColors,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))
|
||||
) {
|
||||
Text(text = "Import")
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
@@ -588,6 +706,46 @@ class HomeSettings : Routes.Route() {
|
||||
) {
|
||||
Text(translation["view_logger_history_button"])
|
||||
}
|
||||
if (showImportDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showImportDialog = false },
|
||||
title = { Text("Import message logger") },
|
||||
text = { Text("Importing will override your current message logger database. Continue?") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
showImportDialog = false
|
||||
runCatching {
|
||||
activityLauncherHelper.openFile("application/octet-stream") { uri ->
|
||||
runCatching {
|
||||
context.androidContext.contentResolver.openInputStream(uri.toUri())?.use { inputStream ->
|
||||
context.messageLogger.databaseFile.outputStream().use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
} ?: throw IllegalStateException("Unable to open selected file")
|
||||
storedMessagesCount = context.messageLogger.getStoredMessageCount()
|
||||
storedStoriesCount = context.messageLogger.getStoredStoriesCount()
|
||||
context.shortToast(translation["success_toast"])
|
||||
context.log.info("Imported message logger from $uri", "MessageLogger")
|
||||
}.onFailure {
|
||||
context.log.error("Failed to import message logger", it)
|
||||
context.longToast("Import failed: ${it.localizedMessage ?: it.message}")
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.error("Failed to launch import picker", it)
|
||||
context.longToast("Import failed: ${it.localizedMessage ?: it.message}")
|
||||
}
|
||||
}) {
|
||||
Text(translation["button.import"] ?: "Import")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showImportDialog = false }) {
|
||||
Text(translation["button.cancel"])
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,31 +823,23 @@ class HomeSettings : Routes.Route() {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 26.dp)
|
||||
) {
|
||||
var selectedFileType by remember { mutableStateOf(InternalFileHandleType.entries.first()) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 26.dp)
|
||||
) {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
TextField(
|
||||
AestheticDropdownField(
|
||||
value = translation.getOrNull("debug_file_${selectedFileType.name.lowercase()}") ?: selectedFileType.fileName,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
expanded = expanded,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
colors = ExposedDropdownMenuDefaults.textFieldColors(
|
||||
focusedContainerColor = Color.White.copy(alpha = 0.08f),
|
||||
unfocusedContainerColor = Color.White.copy(alpha = 0.06f),
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
||||
onClick = { expanded = true }
|
||||
)
|
||||
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
InternalFileHandleType.entries.forEach { fileType ->
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
package me.eternal.purrfectsnap.ui.manager.pages.home
|
||||
|
||||
import android.view.MotionEvent
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
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.key
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import kotlinx.coroutines.delay
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.random.Random
|
||||
|
||||
class RetroGameScreen : Routes.Route() {
|
||||
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||
val gridWidth = 120
|
||||
val gridHeight = 160
|
||||
val shipSpeed = 1.7f
|
||||
val shipRadius = 4.5f
|
||||
val maxSpeedMultiplier = 2.2f
|
||||
val speedRampPerScore = 0.035f
|
||||
val rng = remember { Random(System.currentTimeMillis()) }
|
||||
val pixelFont = remember { FontFamily.Monospace }
|
||||
|
||||
data class Star(var x: Float, var y: Float, var speed: Float)
|
||||
data class Planet(var x: Float, var y: Float, var radius: Float, var speed: Float)
|
||||
|
||||
var score by remember { mutableIntStateOf(0) }
|
||||
var isGameOver by remember { mutableStateOf(false) }
|
||||
var leftPressed by remember { mutableStateOf(false) }
|
||||
var rightPressed by remember { mutableStateOf(false) }
|
||||
var shipX by remember { mutableFloatStateOf(gridWidth / 2f) }
|
||||
val shipY = gridHeight - 20f
|
||||
|
||||
val stars = remember { mutableListOf<Star>() }
|
||||
val planets = remember { mutableListOf<Planet>() }
|
||||
|
||||
val frameBitmap = remember { ImageBitmap(gridWidth, gridHeight) }
|
||||
val frameCanvas = remember { Canvas(frameBitmap) }
|
||||
val paint = remember { Paint() }
|
||||
var frameTick by remember { mutableIntStateOf(0) }
|
||||
|
||||
fun spawnPlanet(): Planet {
|
||||
val radius = rng.nextInt(6, 14).toFloat()
|
||||
val x = rng.nextInt(radius.roundToInt(), gridWidth - radius.roundToInt()).toFloat()
|
||||
val y = rng.nextInt(-gridHeight, -20).toFloat()
|
||||
val speed = rng.nextInt(10, 20) / 10f
|
||||
return Planet(x = x, y = y, radius = radius, speed = speed)
|
||||
}
|
||||
|
||||
fun resetGame() {
|
||||
score = 0
|
||||
isGameOver = false
|
||||
shipX = gridWidth / 2f
|
||||
stars.clear()
|
||||
planets.clear()
|
||||
repeat(28) {
|
||||
stars.add(
|
||||
Star(
|
||||
x = rng.nextInt(0, gridWidth).toFloat(),
|
||||
y = rng.nextInt(0, gridHeight).toFloat(),
|
||||
speed = rng.nextInt(6, 12) / 10f
|
||||
)
|
||||
)
|
||||
}
|
||||
repeat(4) { planets.add(spawnPlanet()) }
|
||||
}
|
||||
|
||||
fun drawRect(left: Float, top: Float, right: Float, bottom: Float, color: Color) {
|
||||
paint.color = color
|
||||
paint.alpha = 1f
|
||||
frameCanvas.drawRect(Rect(left, top, right, bottom), paint)
|
||||
}
|
||||
|
||||
fun drawCircle(centerX: Float, centerY: Float, radius: Float, color: Color) {
|
||||
paint.color = color
|
||||
frameCanvas.drawCircle(androidx.compose.ui.geometry.Offset(centerX, centerY), radius, paint)
|
||||
}
|
||||
|
||||
fun drawShip() {
|
||||
val sx = shipX.roundToInt().toFloat()
|
||||
drawRect(sx - 2f, shipY, sx + 3f, shipY + 1f, Color(0xFFE7F6FF))
|
||||
drawRect(sx - 3f, shipY + 1f, sx + 4f, shipY + 3f, Color(0xFFE7F6FF))
|
||||
drawRect(sx - 4f, shipY + 3f, sx + 5f, shipY + 6f, Color(0xFFE7F6FF))
|
||||
drawRect(sx - 2f, shipY + 6f, sx + 3f, shipY + 8f, Color(0xFFE7F6FF))
|
||||
drawRect(sx - 1f, shipY + 2f, sx + 2f, shipY + 4f, Color(0xFF8AF4FF))
|
||||
if (!isGameOver) {
|
||||
drawRect(sx - 1f, shipY + 8f, sx + 2f, shipY + 10f, Color(0xFFFFB24D))
|
||||
drawRect(sx, shipY + 10f, sx + 1f, shipY + 12f, Color(0xFFFF6B6B))
|
||||
}
|
||||
}
|
||||
|
||||
fun renderFrame() {
|
||||
drawRect(0f, 0f, gridWidth.toFloat(), gridHeight.toFloat(), Color(0xFF1B1440))
|
||||
drawRect(0f, 0f, gridWidth.toFloat(), 14f, Color(0xFF5236B8))
|
||||
stars.forEach { star ->
|
||||
drawRect(star.x, star.y, star.x + 1f, star.y + 1f, Color(0xFFEEE9FF))
|
||||
}
|
||||
planets.forEach { planet ->
|
||||
drawCircle(planet.x, planet.y, planet.radius, Color(0xFF4FD4FF))
|
||||
drawCircle(planet.x - planet.radius * 0.3f, planet.y - planet.radius * 0.2f, planet.radius * 0.35f, Color(0xFFB0F0FF))
|
||||
}
|
||||
drawShip()
|
||||
if (isGameOver) {
|
||||
drawRect(0f, gridHeight / 2f - 10f, gridWidth.toFloat(), gridHeight / 2f + 8f, Color(0xAA000000))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
resetGame()
|
||||
while (true) {
|
||||
delay(16)
|
||||
if (!isGameOver) {
|
||||
val speedMultiplier = min(
|
||||
maxSpeedMultiplier,
|
||||
1f + (score * speedRampPerScore)
|
||||
)
|
||||
val direction = (if (leftPressed) -1f else 0f) + (if (rightPressed) 1f else 0f)
|
||||
shipX = (shipX + direction * shipSpeed).coerceIn(8f, gridWidth - 8f)
|
||||
stars.forEach { star ->
|
||||
star.y += star.speed * speedMultiplier
|
||||
if (star.y > gridHeight) {
|
||||
star.y = 0f
|
||||
star.x = rng.nextInt(0, gridWidth).toFloat()
|
||||
}
|
||||
}
|
||||
planets.forEachIndexed { index, planet ->
|
||||
planet.y += planet.speed * speedMultiplier
|
||||
if (planet.y - planet.radius > gridHeight) {
|
||||
planets[index] = spawnPlanet()
|
||||
score += 1
|
||||
}
|
||||
}
|
||||
planets.forEach { planet ->
|
||||
val dx = abs(planet.x - shipX)
|
||||
val dy = abs(planet.y - shipY)
|
||||
if (dx * dx + dy * dy < (planet.radius + shipRadius) * (planet.radius + shipRadius)) {
|
||||
isGameOver = true
|
||||
}
|
||||
}
|
||||
}
|
||||
renderFrame()
|
||||
frameTick++
|
||||
}
|
||||
}
|
||||
|
||||
val bottomPadding = routes.bottomPadding +
|
||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +
|
||||
24.dp
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = bottomPadding),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
FloatingTopBar(
|
||||
title = "Retro Flight",
|
||||
onBack = { routes.navController.popBackStack() }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 18.dp)
|
||||
.fillMaxWidth()
|
||||
.weight(1f, fill = true)
|
||||
.aspectRatio(3f / 4f, matchHeightConstraintsFirst = true)
|
||||
.border(2.dp, Color.White.copy(alpha = 0.7f), RoundedCornerShape(14.dp))
|
||||
.background(Color(0xFF21195A), RoundedCornerShape(14.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
key(frameTick) {
|
||||
Image(
|
||||
bitmap = frameBitmap,
|
||||
contentDescription = null,
|
||||
filterQuality = FilterQuality.None,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.border(1.dp, Color.White.copy(alpha = 0.1f))
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = score.toString(),
|
||||
color = Color.White,
|
||||
fontSize = 16.sp,
|
||||
fontFamily = pixelFont,
|
||||
modifier = Modifier.align(Alignment.TopCenter).padding(top = 6.dp)
|
||||
)
|
||||
if (isGameOver) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
) {
|
||||
Text(
|
||||
text = "GAME OVER",
|
||||
color = Color.White,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = pixelFont
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Button(
|
||||
onClick = { resetGame() },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF5A43D6),
|
||||
contentColor = Color.White
|
||||
),
|
||||
shape = RoundedCornerShape(6.dp)
|
||||
) {
|
||||
Text("RESTART", fontFamily = pixelFont, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 18.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp)
|
||||
.pointerInteropFilter {
|
||||
when (it.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
leftPressed = true
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
leftPressed = false
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
color = Color(0xFF2E2568),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.2f))
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = "LEFT",
|
||||
color = Color.White,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = pixelFont,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp)
|
||||
.pointerInteropFilter {
|
||||
when (it.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
rightPressed = true
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
rightPressed = false
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
color = Color(0xFF2E2568),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.2f))
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = "RIGHT",
|
||||
color = Color.White,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = pixelFont,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -109,7 +110,14 @@ class ManageScriptReposSection : Routes.Route() {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = { showAddDialog = true },
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.34f),
|
||||
contentColor = Color.White
|
||||
contentColor = Color.White,
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 0.dp,
|
||||
pressedElevation = 0.dp,
|
||||
focusedElevation = 0.dp,
|
||||
hoveredElevation = 0.dp
|
||||
)
|
||||
) {
|
||||
Icon(Icons.Default.Public, contentDescription = null, tint = Color.White)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
@@ -27,7 +27,6 @@ import me.eternal.purrfectsnap.RemoteSideContext
|
||||
import me.eternal.purrfectsnap.common.ReceiversConfig
|
||||
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
|
||||
import me.eternal.purrfectsnap.common.data.MessagingGroupInfo
|
||||
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState
|
||||
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie
|
||||
import me.eternal.purrfectsnap.common.util.snap.SnapWidgetBroadcastReceiverHelper
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
@@ -45,7 +44,7 @@ class AddFriendDialog(
|
||||
val getGroupState: (group: MessagingGroupInfo) -> Boolean,
|
||||
)
|
||||
|
||||
private val stateCache = mutableMapOf<String, Boolean>()
|
||||
private val stateCache = mutableStateMapOf<String, Boolean>()
|
||||
private val translation by lazy { context.translation.getCategory("manager.dialogs.add_friend")}
|
||||
|
||||
@Composable
|
||||
@@ -57,9 +56,13 @@ class AddFriendDialog(
|
||||
getCurrentState: () -> Boolean,
|
||||
onState: (Boolean) -> Unit = {},
|
||||
) {
|
||||
var currentState by rememberAsyncMutableState(defaultValue = stateCache[id] ?: false) {
|
||||
getCurrentState().also { stateCache[id] = it }
|
||||
val cachedState = stateCache[id]
|
||||
LaunchedEffect(id, cachedState) {
|
||||
if (cachedState == null) {
|
||||
stateCache[id] = getCurrentState()
|
||||
}
|
||||
}
|
||||
val currentState = stateCache[id] ?: false
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val cardShape = RoundedCornerShape(18.dp)
|
||||
@@ -68,9 +71,9 @@ class AddFriendDialog(
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp)
|
||||
.clickable {
|
||||
currentState = !currentState
|
||||
stateCache[id] = currentState
|
||||
coroutineScope.launch(Dispatchers.IO) { onState(currentState) }
|
||||
val nextState = !currentState
|
||||
stateCache[id] = nextState
|
||||
coroutineScope.launch(Dispatchers.IO) { onState(nextState) }
|
||||
},
|
||||
shape = cardShape,
|
||||
color = Color.Transparent,
|
||||
@@ -125,9 +128,8 @@ class AddFriendDialog(
|
||||
Switch(
|
||||
checked = currentState,
|
||||
onCheckedChange = {
|
||||
currentState = it
|
||||
stateCache[id] = currentState
|
||||
coroutineScope.launch(Dispatchers.IO) { onState(currentState) }
|
||||
stateCache[id] = it
|
||||
coroutineScope.launch(Dispatchers.IO) { onState(it) }
|
||||
},
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = Color.White,
|
||||
@@ -156,19 +158,12 @@ class AddFriendDialog(
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 18.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(
|
||||
text = translation["title"],
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = translation["search_hint"],
|
||||
fontSize = 13.sp,
|
||||
color = Color.White.copy(alpha = 0.85f)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = translation["title"],
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
Surface(
|
||||
@@ -185,10 +180,11 @@ class AddFriendDialog(
|
||||
value = searchKeyword.value,
|
||||
onValueChange = { searchKeyword.value = it },
|
||||
placeholder = {
|
||||
Text(text = translation["search_hint"])
|
||||
Text(text = translation["search_hint"], color = PurrfectPalette.textSecondary)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
leadingIcon = {
|
||||
Icon(Icons.Filled.Search, contentDescription = translation["search_icon_description"])
|
||||
@@ -198,8 +194,15 @@ class AddFriendDialog(
|
||||
unfocusedContainerColor = PurrfectPalette.cardOverlayColor.copy(alpha = 0.8f),
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
cursorColor = Color.White
|
||||
)
|
||||
cursorColor = Color.White,
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
focusedLeadingIconColor = Color.White,
|
||||
unfocusedLeadingIconColor = Color.White.copy(alpha = 0.85f),
|
||||
focusedPlaceholderColor = PurrfectPalette.textSecondary,
|
||||
unfocusedPlaceholderColor = PurrfectPalette.textSecondary
|
||||
),
|
||||
textStyle = LocalTextStyle.current.copy(color = Color.White)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -302,6 +305,15 @@ 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 ->
|
||||
stateCache[friend.userId] ?: actionHandler.getFriendState(friend)
|
||||
}
|
||||
}
|
||||
}
|
||||
val hasFriendsSelected = selectedFriendCount > 0
|
||||
val allFriendsSelected = filteredFriends.isNotEmpty() && selectedFriendCount == filteredFriends.size
|
||||
|
||||
DialogHeader(searchKeyword)
|
||||
|
||||
@@ -337,14 +349,63 @@ class AddFriendDialog(
|
||||
|
||||
item {
|
||||
if (filteredFriends.isNotEmpty()) {
|
||||
Text(
|
||||
text = translation["category_friends"],
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp, top = 14.dp),
|
||||
color = Color.White
|
||||
)
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = translation["category_friends"],
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
filteredFriends.forEach { friend ->
|
||||
stateCache[friend.userId] = true
|
||||
actionHandler.onFriendState(friend, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !allFriendsSelected
|
||||
) {
|
||||
Text(
|
||||
text = context.translation["manager.dialogs.messaging_action.select_all_button"]
|
||||
?: "Select All",
|
||||
color = if (allFriendsSelected) {
|
||||
Color.White.copy(alpha = 0.45f)
|
||||
} else {
|
||||
PurrfectPalette.glowSecondary
|
||||
}
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
filteredFriends.forEach { friend ->
|
||||
stateCache[friend.userId] = false
|
||||
actionHandler.onFriendState(friend, false)
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = hasFriendsSelected
|
||||
) {
|
||||
Text(
|
||||
text = translation["unselect_all_button"],
|
||||
color = if (hasFriendsSelected) {
|
||||
PurrfectPalette.glowPrimary
|
||||
} else {
|
||||
Color.White.copy(alpha = 0.45f)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,23 @@ import android.content.Intent
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
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.filled.Lock
|
||||
import androidx.compose.material.icons.rounded.DeleteForever
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -46,6 +51,7 @@ import me.eternal.purrfectsnap.ui.util.AlertDialogs
|
||||
import me.eternal.purrfectsnap.ui.util.Dialog
|
||||
import me.eternal.purrfectsnap.ui.util.purrfectSwitchColors
|
||||
import me.eternal.purrfectsnap.ui.util.coil.BitmojiImage
|
||||
import me.eternal.purrfectsnap.ui.util.scaleOnPress
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@@ -72,7 +78,7 @@ class ManageScope: Routes.Route() {
|
||||
var deleteConfirmDialog by remember { mutableStateOf(false) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val titleText by rememberAsyncMutableState<String?>(null, keys = arrayOf(id, scope)) {
|
||||
val titleText by rememberAsyncMutableState<String?>(null, keys = arrayOf<Any>(id, scope)) {
|
||||
when (scope) {
|
||||
SocialScope.FRIEND -> context.database.getFriendInfo(id)?.displayName
|
||||
SocialScope.GROUP -> context.database.getGroupInfo(id)?.name
|
||||
@@ -304,6 +310,61 @@ class ManageScope: Routes.Route() {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.E2eeActionButton(
|
||||
label: String,
|
||||
icon: ImageVector,
|
||||
accent: Brush,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.scaleOnPress(interactionSource)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null
|
||||
) { onClick() },
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = Color.White.copy(alpha = 0.06f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.18f)),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.background(accent, RoundedCornerShape(10.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = label,
|
||||
color = Color.White,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeStreakETA(timestamp: Long): String? {
|
||||
val now = System.currentTimeMillis()
|
||||
val stringBuilder = StringBuilder()
|
||||
@@ -344,7 +405,6 @@ class ManageScope: Routes.Route() {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (context.config.root.experimental.e2eEncryption.globalState == true) {
|
||||
SectionTitle(translation["e2ee_title"])
|
||||
var hasSecretKey by rememberAsyncMutableState(defaultValue = false) {
|
||||
context.e2eeImplementation.friendKeyExists(friend.userId)
|
||||
}
|
||||
@@ -378,40 +438,100 @@ class ManageScope: Routes.Route() {
|
||||
}
|
||||
|
||||
ContentCard {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
if (hasSecretKey) {
|
||||
OutlinedButton(onClick = {
|
||||
context.coroutineScope.launch {
|
||||
val secretKey = Base64.encode(context.e2eeImplementation.getSharedSecretKey(friend.userId) ?: return@launch)
|
||||
//TODO: fingerprint auth
|
||||
context.activity!!.startActivity(Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, secretKey)
|
||||
type = "text/plain"
|
||||
}, "").apply {
|
||||
putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(
|
||||
Intent().apply {
|
||||
putExtra(Intent.EXTRA_TEXT, secretKey)
|
||||
putExtra(Intent.EXTRA_SUBJECT, secretKey)
|
||||
})
|
||||
)
|
||||
})
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.16f))
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.5f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.4f)
|
||||
)
|
||||
),
|
||||
RoundedCornerShape(14.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Lock,
|
||||
contentDescription = null,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}) {
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = translation["export_base64_button"],
|
||||
maxLines = 1
|
||||
text = translation["e2ee_title"],
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
Text(
|
||||
text = translation["e2ee_subtitle"],
|
||||
color = PurrfectPalette.textSecondary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
if (hasSecretKey) {
|
||||
E2eeActionButton(
|
||||
label = translation["export_base64_button"],
|
||||
icon = Icons.Filled.Lock,
|
||||
accent = Brush.horizontalGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.6f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.55f)
|
||||
)
|
||||
),
|
||||
onClick = {
|
||||
context.coroutineScope.launch {
|
||||
val secretKey = Base64.encode(context.e2eeImplementation.getSharedSecretKey(friend.userId) ?: return@launch)
|
||||
//TODO: fingerprint auth
|
||||
context.activity!!.startActivity(Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, secretKey)
|
||||
type = "text/plain"
|
||||
}, "").apply {
|
||||
putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(
|
||||
Intent().apply {
|
||||
putExtra(Intent.EXTRA_TEXT, secretKey)
|
||||
putExtra(Intent.EXTRA_SUBJECT, secretKey)
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedButton(onClick = { importDialog = true }) {
|
||||
Text(
|
||||
text = translation["import_base64_button"],
|
||||
maxLines = 1
|
||||
E2eeActionButton(
|
||||
label = translation["import_base64_button"],
|
||||
icon = Icons.Filled.Lock,
|
||||
accent = Brush.horizontalGradient(
|
||||
listOf(
|
||||
Color(0xFF7DD3FC),
|
||||
Color(0xFF818CF8)
|
||||
)
|
||||
),
|
||||
onClick = { importDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,23 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Groups
|
||||
import androidx.compose.material.icons.filled.People
|
||||
import androidx.compose.material.icons.filled.RemoveRedEye
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -54,8 +59,16 @@ class SocialRootSection : Routes.Route() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScopeList(scope: SocialScope) {
|
||||
private fun ScopeList(
|
||||
scope: SocialScope,
|
||||
friends: List<MessagingFriendInfo>,
|
||||
groups: List<MessagingGroupInfo>
|
||||
) {
|
||||
val remainingHours = remember { context.config.root.streaksReminder.remainingHours.get() }
|
||||
val list = when (scope) {
|
||||
SocialScope.GROUP -> groups
|
||||
SocialScope.FRIEND -> friends
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
@@ -64,10 +77,7 @@ class SocialRootSection : Routes.Route() {
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
//check if scope list is empty
|
||||
val listSize = when (scope) {
|
||||
SocialScope.GROUP -> groupList.size
|
||||
SocialScope.FRIEND -> friendList.size
|
||||
}
|
||||
val listSize = list.size
|
||||
|
||||
if (listSize == 0) {
|
||||
item {
|
||||
@@ -76,14 +86,14 @@ class SocialRootSection : Routes.Route() {
|
||||
}
|
||||
|
||||
items(listSize) { index ->
|
||||
val id = when (scope) {
|
||||
SocialScope.GROUP -> groupList[index].conversationId
|
||||
SocialScope.FRIEND -> friendList[index].userId
|
||||
}
|
||||
val friend = if (scope == SocialScope.FRIEND) list[index] as MessagingFriendInfo else null
|
||||
val group = if (scope == SocialScope.GROUP) list[index] as MessagingGroupInfo else null
|
||||
val id = friend?.userId ?: group?.conversationId.orEmpty()
|
||||
|
||||
SocialCard(
|
||||
scope = scope,
|
||||
index = index,
|
||||
friend = friend,
|
||||
group = group,
|
||||
onManage = {
|
||||
routes.manageScope.navigate {
|
||||
put("id", id)
|
||||
@@ -182,10 +192,30 @@ class SocialRootSection : Routes.Route() {
|
||||
}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val pagerState = rememberPagerState { titles.size }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
updateScopeLists()
|
||||
}
|
||||
val normalizedQuery = remember(searchQuery) { searchQuery.trim() }
|
||||
val filteredFriends = remember(friendList, normalizedQuery) {
|
||||
if (normalizedQuery.isBlank()) {
|
||||
friendList
|
||||
} else {
|
||||
friendList.filter {
|
||||
it.mutableUsername.contains(normalizedQuery, ignoreCase = true) ||
|
||||
it.displayName?.contains(normalizedQuery, ignoreCase = true) == true
|
||||
}
|
||||
}
|
||||
}
|
||||
val filteredGroups = remember(groupList, normalizedQuery) {
|
||||
if (normalizedQuery.isBlank()) {
|
||||
groupList
|
||||
} else {
|
||||
groupList.filter { it.name.contains(normalizedQuery, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -199,8 +229,78 @@ class SocialRootSection : Routes.Route() {
|
||||
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||
},
|
||||
friendCount = friendList.size,
|
||||
groupCount = groupList.size
|
||||
groupCount = groupList.size,
|
||||
searchActive = searchActive,
|
||||
onSearchToggle = {
|
||||
searchActive = !searchActive
|
||||
if (!searchActive) searchQuery = ""
|
||||
}
|
||||
)
|
||||
if (searchActive) {
|
||||
val searchHint = context.translation["manager.dialogs.add_friend.search_hint"] ?: "Search"
|
||||
val searchShape = RoundedCornerShape(18.dp)
|
||||
val searchBorder = Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.45f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 6.dp),
|
||||
shape = searchShape,
|
||||
color = Color.White.copy(alpha = 0.05f),
|
||||
border = BorderStroke(1.dp, searchBorder),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(PurrfectPalette.cardOverlay, searchShape)
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Search,
|
||||
contentDescription = searchHint,
|
||||
tint = PurrfectPalette.textSecondary
|
||||
)
|
||||
BasicTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
singleLine = true,
|
||||
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = Color.White,
|
||||
fontSize = 15.sp
|
||||
),
|
||||
cursorBrush = SolidColor(PurrfectPalette.glowSecondary),
|
||||
modifier = Modifier.weight(1f)
|
||||
) { innerTextField ->
|
||||
if (searchQuery.isEmpty()) {
|
||||
Text(
|
||||
text = searchHint,
|
||||
color = PurrfectPalette.textSecondary,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
IconButton(onClick = { searchQuery = "" }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = context.translation["close_button_description"]
|
||||
?: "Clear search",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
HorizontalPager(
|
||||
modifier = Modifier
|
||||
@@ -208,8 +308,8 @@ class SocialRootSection : Routes.Route() {
|
||||
state = pagerState
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> ScopeList(SocialScope.FRIEND)
|
||||
1 -> ScopeList(SocialScope.GROUP)
|
||||
0 -> ScopeList(SocialScope.FRIEND, filteredFriends, filteredGroups)
|
||||
1 -> ScopeList(SocialScope.GROUP, filteredFriends, filteredGroups)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,7 +318,8 @@ class SocialRootSection : Routes.Route() {
|
||||
@Composable
|
||||
private fun SocialCard(
|
||||
scope: SocialScope,
|
||||
index: Int,
|
||||
friend: MessagingFriendInfo?,
|
||||
group: MessagingGroupInfo?,
|
||||
onManage: () -> Unit,
|
||||
onPreview: () -> Unit,
|
||||
remainingHours: Int
|
||||
@@ -249,7 +350,7 @@ class SocialRootSection : Routes.Route() {
|
||||
) {
|
||||
when (scope) {
|
||||
SocialScope.GROUP -> {
|
||||
val group = groupList[index]
|
||||
val groupInfo = group ?: return@Row
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
@@ -267,7 +368,7 @@ class SocialRootSection : Routes.Route() {
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = group.name,
|
||||
text = groupInfo.name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
@@ -283,16 +384,16 @@ class SocialRootSection : Routes.Route() {
|
||||
}
|
||||
|
||||
SocialScope.FRIEND -> {
|
||||
val friend = friendList[index]
|
||||
val streaks by rememberAsyncMutableState(defaultValue = friend.streaks) {
|
||||
context.database.getFriendStreaks(friend.userId)
|
||||
val friendInfo = friend ?: return@Row
|
||||
val streaks by rememberAsyncMutableState(defaultValue = friendInfo.streaks) {
|
||||
context.database.getFriendStreaks(friendInfo.userId)
|
||||
}
|
||||
|
||||
BitmojiImage(
|
||||
context = context,
|
||||
url = BitmojiSelfie.getBitmojiSelfie(
|
||||
friend.selfieId,
|
||||
friend.bitmojiId,
|
||||
friendInfo.selfieId,
|
||||
friendInfo.bitmojiId,
|
||||
BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D
|
||||
)
|
||||
)
|
||||
@@ -302,7 +403,7 @@ class SocialRootSection : Routes.Route() {
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = friend.displayName ?: friend.mutableUsername,
|
||||
text = friendInfo.displayName ?: friendInfo.mutableUsername,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
@@ -310,7 +411,7 @@ class SocialRootSection : Routes.Route() {
|
||||
fontSize = 15.sp
|
||||
)
|
||||
Text(
|
||||
text = friend.mutableUsername,
|
||||
text = friendInfo.mutableUsername,
|
||||
maxLines = 1,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
@@ -383,7 +484,9 @@ class SocialRootSection : Routes.Route() {
|
||||
pagerState: androidx.compose.foundation.pager.PagerState,
|
||||
onTabSelected: (Int) -> Unit,
|
||||
friendCount: Int,
|
||||
groupCount: Int
|
||||
groupCount: Int,
|
||||
searchActive: Boolean,
|
||||
onSearchToggle: () -> Unit
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
@@ -431,6 +534,13 @@ class SocialRootSection : Routes.Route() {
|
||||
) {
|
||||
StatPill(label = "Friends", value = friendCount)
|
||||
StatPill(label = "Groups", value = groupCount)
|
||||
IconButton(onClick = onSearchToggle) {
|
||||
Icon(
|
||||
imageVector = if (searchActive) Icons.Filled.Close else Icons.Filled.Search,
|
||||
contentDescription = if (searchActive) "Close search" else "Search",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SocialTabSwitcher(
|
||||
|
||||
@@ -54,6 +54,14 @@ import me.eternal.purrfectsnap.ui.manager.pages.social.AddFriendDialog
|
||||
class EditRule : Routes.Route() {
|
||||
override val translation by lazy { context.translation.getCategory("manager.friend_tracker") }
|
||||
|
||||
private data class RuleSnapshot(
|
||||
val name: String,
|
||||
val author: String,
|
||||
val scopes: List<String>,
|
||||
val events: List<TrackerRuleEvent>,
|
||||
val scopeType: TrackerScopeType
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun RuleCard(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -174,11 +182,11 @@ class EditRule : Routes.Route() {
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded.value,
|
||||
onExpandedChange = { expanded.value = !expanded.value },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded.value,
|
||||
onExpandedChange = { expanded.value = !expanded.value },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
val eventLabel = context.translation["tracker_events.${currentEventType.value}"]
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -186,8 +194,8 @@ class EditRule : Routes.Route() {
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.widthIn(min = 240.dp),
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||
.wrapContentWidth(),
|
||||
value = eventLabel,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
@@ -207,7 +215,7 @@ class EditRule : Routes.Route() {
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded.value,
|
||||
onDismissRequest = { expanded.value = false },
|
||||
modifier = Modifier.widthIn(min = 240.dp),
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
containerColor = Color(0xFF121528),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
) {
|
||||
@@ -295,24 +303,45 @@ class EditRule : Routes.Route() {
|
||||
val authorName = rememberAsyncMutableState<String>(defaultValue = "", keys = arrayOf(currentRuleId)) {
|
||||
currentRuleId?.let { ruleId -> context.database.getTrackerRule(ruleId)?.author ?: "" } ?: ""
|
||||
}
|
||||
val initialRuleState by remember(ruleName.value.isNotBlank() || events.isNotEmpty() || scopes.isNotEmpty()) {
|
||||
mutableStateOf(
|
||||
mapOf(
|
||||
"name" to ruleName.value,
|
||||
"author" to authorName.value,
|
||||
"scopes" to scopes.toList(),
|
||||
"events" to events.toList(),
|
||||
"scopeType" to currentScopeType
|
||||
)
|
||||
)
|
||||
fun snapshotEvents() = events.map { event ->
|
||||
event.copy(params = event.params.copy(), actions = event.actions.toList())
|
||||
}
|
||||
fun buildSnapshot() = RuleSnapshot(
|
||||
name = ruleName.value,
|
||||
author = authorName.value,
|
||||
scopes = scopes.toList(),
|
||||
events = snapshotEvents(),
|
||||
scopeType = currentScopeType
|
||||
)
|
||||
val initialSnapshot = remember(currentRuleId) {
|
||||
mutableStateOf<RuleSnapshot?>(if (currentRuleId == null) buildSnapshot() else null)
|
||||
}
|
||||
LaunchedEffect(
|
||||
currentRuleId,
|
||||
ruleName.value,
|
||||
authorName.value,
|
||||
events.size,
|
||||
scopes.size,
|
||||
currentScopeType
|
||||
) {
|
||||
if (initialSnapshot.value == null) {
|
||||
val hasLoaded = ruleName.value.isNotBlank() ||
|
||||
authorName.value.isNotBlank() ||
|
||||
events.isNotEmpty() ||
|
||||
scopes.isNotEmpty()
|
||||
if (hasLoaded) {
|
||||
initialSnapshot.value = buildSnapshot()
|
||||
}
|
||||
}
|
||||
}
|
||||
val isDirty by remember {
|
||||
derivedStateOf {
|
||||
initialRuleState["name"] != ruleName.value ||
|
||||
initialRuleState["author"] != authorName.value ||
|
||||
initialRuleState["scopes"] != scopes.toList() ||
|
||||
initialRuleState["events"] != events.toList() ||
|
||||
initialRuleState["scopeType"] != currentScopeType
|
||||
val snapshot = initialSnapshot.value ?: return@derivedStateOf false
|
||||
snapshot.name != ruleName.value ||
|
||||
snapshot.author != authorName.value ||
|
||||
snapshot.scopes != scopes.toList() ||
|
||||
snapshot.events != snapshotEvents() ||
|
||||
snapshot.scopeType != currentScopeType
|
||||
}
|
||||
}
|
||||
var deleteConfirmation by remember { mutableStateOf(false) }
|
||||
@@ -426,6 +455,12 @@ class EditRule : Routes.Route() {
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
if (currentRuleId != null) {
|
||||
IconButton(onClick = { deleteConfirmation = true }) {
|
||||
Icon(Icons.Default.DeleteOutline, contentDescription = translation["delete_button_description"], tint = Color.White)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
}
|
||||
IconButton(onClick = {
|
||||
if (events.isEmpty()) {
|
||||
showEventsEmptyDialog = true
|
||||
@@ -451,11 +486,6 @@ class EditRule : Routes.Route() {
|
||||
context.database.setRuleTrackerScopes(ruleId, currentScopeType, scopes)
|
||||
routes.navController.popBackStack()
|
||||
}) { Icon(Icons.Filled.Save, contentDescription = translation["save_button_description"], tint = Color.White) }
|
||||
if (currentRuleId != null) {
|
||||
IconButton(onClick = { deleteConfirmation = true }) {
|
||||
Icon(Icons.Default.DeleteOutline, contentDescription = translation["delete_button_description"], tint = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,9 +496,11 @@ class EditRule : Routes.Route() {
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
.padding(padding)
|
||||
) {
|
||||
val contentBottomPadding = routes.bottomPadding + 12.dp
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = contentBottomPadding)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
@@ -477,6 +509,7 @@ class EditRule : Routes.Route() {
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
val textFieldShape = RoundedCornerShape(16.dp)
|
||||
Text(
|
||||
translation["general_section_title"],
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
@@ -488,8 +521,11 @@ class EditRule : Routes.Route() {
|
||||
value = ruleName.value,
|
||||
onValueChange = { ruleName.value = it },
|
||||
label = { Text(translation["rule_name_label"], color = PurrfectPalette.textSecondary) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 2.dp),
|
||||
singleLine = true,
|
||||
shape = textFieldShape,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.White.copy(alpha = 0.05f),
|
||||
unfocusedContainerColor = Color.White.copy(alpha = 0.04f),
|
||||
@@ -506,8 +542,11 @@ class EditRule : Routes.Route() {
|
||||
value = authorName.value,
|
||||
onValueChange = { authorName.value = it },
|
||||
label = { Text(translation["author_name_label"], color = PurrfectPalette.textSecondary) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 2.dp),
|
||||
singleLine = true,
|
||||
shape = textFieldShape,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.White.copy(alpha = 0.05f),
|
||||
unfocusedContainerColor = Color.White.copy(alpha = 0.04f),
|
||||
@@ -640,12 +679,21 @@ class EditRule : Routes.Route() {
|
||||
shape = selectorShape,
|
||||
color = Color.Transparent,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 10.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.14f))
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.45f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.38f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(selectorBrush, selectorShape)
|
||||
.clip(selectorShape)
|
||||
.background(selectorBrush)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
@@ -773,7 +821,6 @@ class EditRule : Routes.Route() {
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(routes.bottomPadding))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,26 +11,24 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.DateRange
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.filled.SaveAlt
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
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.TextStyle
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
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 java.util.Date
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
@@ -328,36 +326,50 @@ fun LogsTab(
|
||||
var dropDownExpanded by remember { mutableStateOf(false) }
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(selectionExpanded.value) {
|
||||
if (!selectionExpanded.value) {
|
||||
dropDownExpanded = false
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(showDatePicker) {
|
||||
if (showDatePicker) {
|
||||
sinceDatePickerState.displayMode = DisplayMode.Picker
|
||||
}
|
||||
}
|
||||
|
||||
if (showDatePicker) {
|
||||
Dialog(onDismissRequest = { showDatePicker = false }) {
|
||||
val dialogShape = RoundedCornerShape(22.dp)
|
||||
Surface(
|
||||
shape = dialogShape,
|
||||
color = Color.Transparent,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.14f)),
|
||||
shadowElevation = 16.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay, dialogShape)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
AestheticDialog(
|
||||
onDismissRequest = { showDatePicker = false },
|
||||
title = "",
|
||||
text = "",
|
||||
icon = Icons.Default.DateRange,
|
||||
confirmButtonText = context.translation["button.ok"],
|
||||
onConfirm = { showDatePicker = false },
|
||||
dismissButtonText = context.translation["button.cancel"],
|
||||
onDismiss = {
|
||||
showDatePicker = false
|
||||
sinceDatePickerState.selectedDateMillis = null
|
||||
},
|
||||
customContent = {
|
||||
Surface(
|
||||
modifier = Modifier.padding(horizontal = 6.dp),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = PurrfectPalette.cardOverlayColor,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))
|
||||
) {
|
||||
Text(
|
||||
translation["pick_a_date_button"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
DatePicker(
|
||||
state = sinceDatePickerState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.White.copy(alpha = 0.02f), RoundedCornerShape(18.dp)),
|
||||
.widthIn(max = 360.dp)
|
||||
.padding(6.dp)
|
||||
.background(PurrfectPalette.cardOverlayColor, RoundedCornerShape(14.dp)),
|
||||
title = null,
|
||||
headline = null,
|
||||
showModeToggle = false,
|
||||
colors = DatePickerDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
containerColor = PurrfectPalette.cardOverlayColor,
|
||||
titleContentColor = Color.White,
|
||||
headlineContentColor = Color.White,
|
||||
weekdayContentColor = Color.White.copy(alpha = 0.9f),
|
||||
@@ -368,45 +380,35 @@ fun LogsTab(
|
||||
todayDateBorderColor = PurrfectPalette.glowSecondary,
|
||||
dayContentColor = Color.White.copy(alpha = 0.85f),
|
||||
disabledDayContentColor = Color.White.copy(alpha = 0.35f),
|
||||
yearContentColor = Color.White,
|
||||
currentYearContentColor = PurrfectPalette.glowSecondary,
|
||||
selectedYearContainerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.4f),
|
||||
selectedYearContentColor = Color.Black,
|
||||
dividerColor = Color.White.copy(alpha = 0.14f),
|
||||
navigationContentColor = Color.White
|
||||
navigationContentColor = Color.White,
|
||||
dateTextFieldColors = TextFieldDefaults.colors(
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
disabledTextColor = Color.White.copy(alpha = 0.6f),
|
||||
focusedContainerColor = Color.White.copy(alpha = 0.08f),
|
||||
unfocusedContainerColor = Color.White.copy(alpha = 0.06f),
|
||||
disabledContainerColor = Color.White.copy(alpha = 0.04f),
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
cursorColor = PurrfectPalette.glowSecondary,
|
||||
focusedLabelColor = PurrfectPalette.textSecondary,
|
||||
unfocusedLabelColor = PurrfectPalette.textSecondary
|
||||
)
|
||||
)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End)
|
||||
) {
|
||||
Surface(
|
||||
onClick = {
|
||||
showDatePicker = false
|
||||
sinceDatePickerState.selectedDateMillis = null
|
||||
},
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))
|
||||
) {
|
||||
Text(
|
||||
context.translation["button.cancel"],
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
onClick = { showDatePicker = false },
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = PurrfectPalette.glowPrimary.copy(alpha = 0.32f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.14f))
|
||||
) {
|
||||
Text(
|
||||
context.translation["button.ok"],
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
opaque = true,
|
||||
showCloseButton = false,
|
||||
showIcon = false,
|
||||
showTitle = false
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
@@ -443,27 +445,33 @@ fun LogsTab(
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = dropDownExpanded,
|
||||
onExpandedChange = { dropDownExpanded = it },
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier.wrapContentWidth()
|
||||
) {
|
||||
Surface(
|
||||
onClick = { dropDownExpanded = true },
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||
.border(1.dp, Color.White.copy(alpha = 0.2f), RoundedCornerShape(12.dp)),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color.White.copy(alpha = 0.06f)
|
||||
color = Color.White.copy(alpha = 0.06f),
|
||||
tonalElevation = 0.dp
|
||||
) {
|
||||
Text(
|
||||
filterType.name,
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
color = Color.White
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(filterType.name, color = Color.White)
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropDownExpanded)
|
||||
}
|
||||
}
|
||||
ExposedDropdownMenu(
|
||||
expanded = dropDownExpanded,
|
||||
onDismissRequest = { dropDownExpanded = false },
|
||||
containerColor = Color(0xFF101220),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
modifier = Modifier.wrapContentWidth()
|
||||
) {
|
||||
FriendTrackerManagerRoot.FilterType.entries.forEach { type ->
|
||||
DropdownMenuItem(
|
||||
@@ -489,7 +497,6 @@ fun LogsTab(
|
||||
checked = reverseSortOrder,
|
||||
onCheckedChange = {
|
||||
reverseSortOrder = it
|
||||
selectionExpanded.value = false
|
||||
},
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -99,7 +100,14 @@ class ManageFriendTrackerReposSection: Routes.Route() {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = { showAddDialog = true },
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.34f),
|
||||
contentColor = Color.White
|
||||
contentColor = Color.White,
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 0.dp,
|
||||
pressedElevation = 0.dp,
|
||||
focusedElevation = 0.dp,
|
||||
hoveredElevation = 0.dp
|
||||
)
|
||||
) {
|
||||
Icon(Icons.Default.Public, contentDescription = null, tint = Color.White)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
@@ -56,12 +56,14 @@ 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.VerifiedUser
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -89,6 +91,7 @@ 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.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.setup.screens.SetupScreen
|
||||
import me.eternal.purrfectsnap.ui.setup.screens.impl.InstallModeScreen
|
||||
@@ -97,8 +100,10 @@ import me.eternal.purrfectsnap.ui.setup.screens.impl.MappingsScreen
|
||||
import me.eternal.purrfectsnap.ui.setup.screens.impl.PermissionsScreen
|
||||
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.util.scaleOnPress
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private data class SetupStepMeta(
|
||||
val route: String,
|
||||
@@ -125,14 +130,19 @@ class SetupActivity : ComponentActivity() {
|
||||
val isFirstRunFlow = hasRequirement(Requirements.FIRST_RUN) || wasInProgress
|
||||
val persistedRoute = setupPrefs.getString("setup_current_route", null)
|
||||
val persistedSkipPatch = setupPrefs.getBoolean("setup_skip_patch", false)
|
||||
val persistedInstallMode = setupPrefs.getString("setup_install_mode", null)
|
||||
val skipPatchChoice = mutableStateOf(persistedSkipPatch)
|
||||
val installModeChoice = mutableStateOf(
|
||||
runCatching { persistedInstallMode?.let { InstallMode.valueOf(it) } }.getOrNull()
|
||||
)
|
||||
|
||||
fun persistProgress(route: String, skipPatch: Boolean, inProgress: Boolean = true) {
|
||||
fun persistProgress(route: String, skipPatch: Boolean, installMode: InstallMode?, inProgress: Boolean = true) {
|
||||
if (!isFirstRunFlow) return
|
||||
setupPrefs.edit()
|
||||
.putBoolean("setup_in_progress", inProgress)
|
||||
.putString("setup_current_route", route)
|
||||
.putBoolean("setup_skip_patch", skipPatch)
|
||||
.putString("setup_install_mode", installMode?.name)
|
||||
.apply()
|
||||
}
|
||||
|
||||
@@ -141,6 +151,7 @@ class SetupActivity : ComponentActivity() {
|
||||
.remove("setup_in_progress")
|
||||
.remove("setup_current_route")
|
||||
.remove("setup_skip_patch")
|
||||
.remove("setup_install_mode")
|
||||
.apply()
|
||||
}
|
||||
|
||||
@@ -148,11 +159,19 @@ class SetupActivity : ComponentActivity() {
|
||||
if (isFirstRunFlow || hasRequirement(Requirements.LANGUAGE)) {
|
||||
add(PickLanguageScreen().apply { route = "language" })
|
||||
if (isFirstRunFlow) {
|
||||
add(InstallModeScreen { mode ->
|
||||
skipPatchChoice.value = mode == InstallMode.ROOT
|
||||
}.apply { route = "installMode" })
|
||||
add(InstallModeScreen(
|
||||
onModeChosen = { mode ->
|
||||
installModeChoice.value = mode
|
||||
skipPatchChoice.value = false
|
||||
},
|
||||
onSkipAutoSetup = {
|
||||
skipPatchChoice.value = true
|
||||
installModeChoice.value = null
|
||||
}
|
||||
).apply { route = "installMode" })
|
||||
}
|
||||
if (isFirstRunFlow) {
|
||||
add(RootInstallSnapchatScreen().apply { route = "rootInstallSnapchat" })
|
||||
add(PatchSnapchatScreen().apply { route = "patchSnapchat" })
|
||||
}
|
||||
}
|
||||
@@ -173,31 +192,46 @@ class SetupActivity : ComponentActivity() {
|
||||
}
|
||||
requiredScreens.forEach { screen ->
|
||||
screen.context = setupContext
|
||||
screen.isFirstRunFlow = isFirstRunFlow
|
||||
screen.init()
|
||||
}
|
||||
|
||||
if (!isFirstRunFlow) {
|
||||
clearProgress()
|
||||
skipPatchChoice.value = false
|
||||
}
|
||||
if (!isFirstRunFlow) {
|
||||
clearProgress()
|
||||
skipPatchChoice.value = false
|
||||
installModeChoice.value = null
|
||||
}
|
||||
|
||||
setContent {
|
||||
val navController = rememberNavController()
|
||||
var canGoNext by remember { mutableStateOf(false) }
|
||||
var lastRoute by rememberSaveable { mutableStateOf("") }
|
||||
var currentRoute by rememberSaveable {
|
||||
mutableStateOf(
|
||||
when {
|
||||
requiredScreens.first().route == "language" -> requiredScreens.first().route
|
||||
persistedRoute?.takeIf { route -> requiredScreens.any { it.route == route } } != null -> persistedRoute
|
||||
else -> requiredScreens.first().route
|
||||
}
|
||||
persistedRoute?.takeIf { route -> requiredScreens.any { it.route == route } }
|
||||
?: requiredScreens.first().route
|
||||
)
|
||||
}
|
||||
val skipPatch by rememberSaveable { skipPatchChoice }
|
||||
val visibleScreens = remember(skipPatch) {
|
||||
requiredScreens.filterNot { skipPatch && it is PatchSnapchatScreen }
|
||||
val installMode by installModeChoice
|
||||
val visibleScreens = remember(skipPatch, installMode) {
|
||||
requiredScreens.filterNot { screen ->
|
||||
if (skipPatch && (screen is PatchSnapchatScreen || screen is RootInstallSnapchatScreen)) {
|
||||
return@filterNot true
|
||||
}
|
||||
if (installMode == null && (screen is PatchSnapchatScreen || screen is RootInstallSnapchatScreen)) {
|
||||
return@filterNot true
|
||||
}
|
||||
if (installMode == InstallMode.ROOT && screen is PatchSnapchatScreen) {
|
||||
return@filterNot true
|
||||
}
|
||||
if (installMode == InstallMode.NON_ROOT && screen is RootInstallSnapchatScreen) {
|
||||
return@filterNot true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
val stepMeta = remember(skipPatch) { visibleScreens.map { it.meta(setupContext) } }
|
||||
val stepMeta = remember(skipPatch, installMode) { visibleScreens.map { it.meta(setupContext) } }
|
||||
val currentStepIndex = visibleScreens.indexOfFirst { it.route == currentRoute }.let {
|
||||
if (it == -1) 0 else it
|
||||
}
|
||||
@@ -218,15 +252,18 @@ class SetupActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(currentRoute, skipPatch) {
|
||||
persistProgress(currentRoute, skipPatch, true)
|
||||
LaunchedEffect(currentRoute, skipPatch, installMode) {
|
||||
persistProgress(currentRoute, skipPatch, installMode, true)
|
||||
if (navController.currentDestination?.route != currentRoute) {
|
||||
navController.navigate(currentRoute) {
|
||||
popUpTo(requiredScreens.first().route) { inclusive = false }
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
canGoNext = false
|
||||
if (lastRoute != currentRoute) {
|
||||
canGoNext = false
|
||||
lastRoute = currentRoute
|
||||
}
|
||||
}
|
||||
|
||||
fun nextScreen() {
|
||||
@@ -249,10 +286,25 @@ 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--
|
||||
}
|
||||
}
|
||||
}
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
@Suppress("DEPRECATION")
|
||||
window.statusBarColor = Color.Transparent.toArgb()
|
||||
@Suppress("DEPRECATION")
|
||||
window.navigationBarColor = Color.Transparent.toArgb()
|
||||
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
|
||||
insetsController.isAppearanceLightStatusBars = false
|
||||
@@ -263,6 +315,37 @@ class SetupActivity : ComponentActivity() {
|
||||
.fillMaxSize()
|
||||
.background(Color.Transparent)
|
||||
) {
|
||||
if (showImportantDialog) {
|
||||
val confirmLabel = if (importantTimeout > 0) "I understand (${importantTimeout}s)" else "I understand"
|
||||
AestheticDialog(
|
||||
onDismissRequest = {
|
||||
if (importantTimeout == 0) {
|
||||
showImportantDialog = false
|
||||
setupPrefs.edit().putBoolean("setup_important_notice_shown", true).apply()
|
||||
}
|
||||
},
|
||||
title = "Important!",
|
||||
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 = "If you have used SnapEnhance or any other mod besides PurrfectSnap, we recommend uninstalling everything and staying on stock Snapchat for one week. Then switch to PurrfectSnap after next Friday.",
|
||||
color = PurrfectPalette.textSecondary,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
},
|
||||
opaque = true
|
||||
)
|
||||
}
|
||||
SetupAuroraBackground()
|
||||
SetupTopBar()
|
||||
val bottomPadding = 118.dp + navBarPadding
|
||||
@@ -386,6 +469,13 @@ private fun SetupScreen.meta(context: RemoteSideContext): SetupStepMeta {
|
||||
icon = Icons.Filled.Download
|
||||
)
|
||||
|
||||
is RootInstallSnapchatScreen -> SetupStepMeta(
|
||||
route = route,
|
||||
title = "Snapchat Installer",
|
||||
subtitle = "Download and install the recommended Snapchat build.",
|
||||
icon = Icons.Filled.Download
|
||||
)
|
||||
|
||||
is SaveFolderScreen -> SetupStepMeta(
|
||||
route = route,
|
||||
title = translation["setup.dialogs.save_folder"] ?: "Where should we save?",
|
||||
|
||||
@@ -28,6 +28,7 @@ abstract class SetupScreen {
|
||||
lateinit var allowNext: (canGoNext: Boolean) -> Unit
|
||||
lateinit var goNext: () -> Unit
|
||||
lateinit var route: String
|
||||
var isFirstRunFlow: Boolean = false
|
||||
|
||||
@Composable
|
||||
fun DialogText(text: String, modifier: Modifier = Modifier) {
|
||||
|
||||
@@ -9,19 +9,26 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
|
||||
import androidx.compose.material.icons.filled.Shield
|
||||
import androidx.compose.material.icons.filled.VerifiedUser
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -32,9 +39,16 @@ import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
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.foundation.interaction.MutableInteractionSource
|
||||
import kotlinx.coroutines.delay
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.setup.screens.SetupScreen
|
||||
import me.eternal.purrfectsnap.ui.util.scaleOnPress
|
||||
@@ -42,25 +56,140 @@ import me.eternal.purrfectsnap.ui.util.scaleOnPress
|
||||
enum class InstallMode { ROOT, NON_ROOT }
|
||||
|
||||
class InstallModeScreen(
|
||||
private val onModeChosen: (InstallMode) -> Unit
|
||||
private val onModeChosen: (InstallMode) -> Unit,
|
||||
private val onSkipAutoSetup: () -> Unit
|
||||
) : SetupScreen() {
|
||||
private var selectedMode: InstallMode? = null
|
||||
private var skipAutoSetup = false
|
||||
|
||||
override fun init() {
|
||||
selectedMode = null
|
||||
skipAutoSetup = false
|
||||
}
|
||||
|
||||
override fun onLeave() {
|
||||
selectedMode?.let(onModeChosen)
|
||||
if (skipAutoSetup) return
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
var choice by remember { mutableStateOf(selectedMode) }
|
||||
var skipSelected by remember { mutableStateOf(skipAutoSetup) }
|
||||
var showGuides by remember { mutableStateOf(true) }
|
||||
var timeout by remember { mutableIntStateOf(15) }
|
||||
|
||||
LaunchedEffect(choice) {
|
||||
LaunchedEffect(choice, skipSelected) {
|
||||
selectedMode = choice
|
||||
allowNext(choice != null)
|
||||
skipAutoSetup = skipSelected
|
||||
allowNext(choice != null || skipSelected)
|
||||
if (!skipSelected) {
|
||||
choice?.let(onModeChosen)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(showGuides) {
|
||||
if (showGuides) {
|
||||
timeout = 15
|
||||
while (timeout > 0) {
|
||||
delay(1000)
|
||||
timeout--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showGuides) {
|
||||
val confirmLabel = if (timeout > 0) "I understand (${timeout}s)" else "I understand"
|
||||
AestheticDialog(
|
||||
onDismissRequest = { if (timeout == 0) showGuides = false },
|
||||
title = "Please note!",
|
||||
text = "",
|
||||
icon = Icons.Filled.Warning,
|
||||
confirmButtonText = confirmLabel,
|
||||
onConfirm = { if (timeout == 0) showGuides = false },
|
||||
confirmEnabled = timeout == 0,
|
||||
showCloseButton = false,
|
||||
customContent = {
|
||||
val bodyStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = PurrfectPalette.textSecondary,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = PurrfectPalette.cardOverlayColor,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 360.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Select the type of device you have: rooted or non-rooted. If you are unsure, choose Non-root and continue.",
|
||||
style = bodyStyle,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "Non-rooted devices",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
Text(
|
||||
text = "Select Non-root and the app will handle everything. Tap Install Patched Snapchat when it appears. After it installs, do not open Snapchat yet. Continue the PurrfectSnap setup; once it finishes, you can open Snapchat and enjoy.",
|
||||
style = bodyStyle,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "Rooted devices",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
Text(
|
||||
text = "Make sure you have flashed LSPosed first. We recommend JingMatrix LSPosed or LSPosed Irena. After you select Root, the app will install the recommended Snapchat version. Do not open it yet; continue the PurrfectSnap setup. When setup finishes, enable PurrfectSnap in LSPosed and reboot your phone. Then start using Snapchat. We highly recommend detaching Snapchat from the Play Store with the Zygisk Detach module to prevent auto-updates.",
|
||||
style = bodyStyle,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "If you run into any installation issues, the solution will appear here. Please read it carefully.",
|
||||
style = bodyStyle,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold, color = Color.White)) {
|
||||
append("Note: ")
|
||||
}
|
||||
append("New Accounts easily get locked! It is recommended to use an older account with PurrfectSnap.")
|
||||
},
|
||||
style = bodyStyle,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SetupCard {
|
||||
@@ -84,7 +213,10 @@ class InstallModeScreen(
|
||||
)
|
||||
),
|
||||
selected = choice == InstallMode.ROOT,
|
||||
onClick = { choice = InstallMode.ROOT }
|
||||
onClick = {
|
||||
choice = InstallMode.ROOT
|
||||
skipSelected = false
|
||||
}
|
||||
)
|
||||
ModeOption(
|
||||
title = "Non-rooted device",
|
||||
@@ -97,9 +229,54 @@ class InstallModeScreen(
|
||||
)
|
||||
),
|
||||
selected = choice == InstallMode.NON_ROOT,
|
||||
onClick = { choice = InstallMode.NON_ROOT }
|
||||
onClick = {
|
||||
choice = InstallMode.NON_ROOT
|
||||
skipSelected = false
|
||||
}
|
||||
)
|
||||
}
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.scaleOnPress(interactionSource)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null
|
||||
) {
|
||||
skipSelected = true
|
||||
choice = null
|
||||
onSkipAutoSetup()
|
||||
goNext()
|
||||
},
|
||||
color = Color.White.copy(alpha = 0.04f),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.18f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Skip Auto Setup",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos,
|
||||
contentDescription = null,
|
||||
tint = PurrfectPalette.glowSecondary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,21 @@ import androidx.compose.foundation.layout.Column
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -26,11 +34,15 @@ 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.window.Dialog
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.setup.screens.SetupScreen
|
||||
import me.eternal.purrfectsnap.ui.util.AlertDialogs
|
||||
@@ -42,11 +54,21 @@ class MappingsScreen : SetupScreen() {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var infoText by remember { mutableStateOf(null as String?) }
|
||||
var isGenerating by remember { mutableStateOf(false) }
|
||||
var showCompletionNotice by remember { mutableStateOf(false) }
|
||||
var completionCountdown by remember { mutableIntStateOf(10) }
|
||||
|
||||
fun finishMappings() {
|
||||
if (isFirstRunFlow) {
|
||||
showCompletionNotice = true
|
||||
} else {
|
||||
goNext()
|
||||
}
|
||||
}
|
||||
|
||||
if (infoText != null) {
|
||||
fun dismiss() {
|
||||
infoText = null
|
||||
goNext()
|
||||
finishMappings()
|
||||
}
|
||||
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
@@ -64,6 +86,100 @@ class MappingsScreen : SetupScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(showCompletionNotice) {
|
||||
if (showCompletionNotice) {
|
||||
completionCountdown = 10
|
||||
while (completionCountdown > 0) {
|
||||
delay(1000)
|
||||
completionCountdown--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showCompletionNotice) {
|
||||
val confirmLabel = if (completionCountdown > 0) {
|
||||
"I understand (${completionCountdown}s)"
|
||||
} else {
|
||||
"I understand"
|
||||
}
|
||||
AestheticDialog(
|
||||
onDismissRequest = { if (completionCountdown == 0) { showCompletionNotice = false; goNext() } },
|
||||
title = "Please note!",
|
||||
text = "",
|
||||
icon = Icons.Filled.Warning,
|
||||
confirmButtonText = confirmLabel,
|
||||
onConfirm = { if (completionCountdown == 0) { showCompletionNotice = false; goNext() } },
|
||||
confirmEnabled = completionCountdown == 0,
|
||||
showCloseButton = false,
|
||||
customContent = {
|
||||
val bodyStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = PurrfectPalette.textSecondary,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = PurrfectPalette.cardOverlayColor,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 360.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "If you see the \"Account temporarily disabled\" error while logging in, do not worry. Follow these steps in order:",
|
||||
style = bodyStyle,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "1. Reopen Snapchat and log in. This fixes it most of the time.",
|
||||
style = bodyStyle,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "2. If it still fails, tap the login button repeatedly. This usually covers the next chunk.",
|
||||
style = bodyStyle,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "3. If it still fails, clear Snapchat's data, disable any VPN, and log in again.",
|
||||
style = bodyStyle,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "For rooted users:",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
lineHeight = 18.sp
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "Reopen Snapchat and log in. If it still fails, disable PurrfectSnap in LSPosed, log in, then re-enable PurrfectSnap.",
|
||||
style = bodyStyle,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
if (isGenerating) return@launch
|
||||
@@ -83,7 +199,7 @@ class MappingsScreen : SetupScreen() {
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
goNext()
|
||||
finishMappings()
|
||||
}
|
||||
}.onFailure {
|
||||
isGenerating = false
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -19,12 +20,16 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
@@ -52,8 +57,10 @@ import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.FileProvider
|
||||
@@ -66,6 +73,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.eternal.purrfectsnap.setup.patch.AutoPatchServer
|
||||
import me.eternal.purrfectsnap.setup.patch.LSPatch
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.setup.screens.SetupScreen
|
||||
import me.eternal.purrfectsnap.ui.util.scaleOnPress
|
||||
@@ -86,6 +94,8 @@ class PatchSnapchatScreen : SetupScreen() {
|
||||
override fun Content() {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val logs = remember { mutableStateListOf("Auto Patcher is ready.") }
|
||||
@Suppress("DEPRECATION")
|
||||
val clipboard = LocalClipboardManager.current
|
||||
var progress by remember { mutableFloatStateOf(-1f) }
|
||||
var patchedApkPath by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var downloadedApkPath by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
@@ -98,6 +108,7 @@ class PatchSnapchatScreen : SetupScreen() {
|
||||
var installRequested by rememberSaveable { mutableStateOf(false) }
|
||||
var installWatcher by remember { mutableStateOf<Job?>(null) }
|
||||
var downloadFinished by rememberSaveable { mutableStateOf(false) }
|
||||
var showIssuesDialog by remember { mutableStateOf(false) }
|
||||
val logPulse by rememberInfiniteTransition(label = "logPulse").animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
@@ -112,7 +123,7 @@ class PatchSnapchatScreen : SetupScreen() {
|
||||
LaunchedEffect(installVerified) { allowNext(installVerified) }
|
||||
|
||||
fun pushLog(message: String) {
|
||||
if (logs.size > 16) logs.removeFirst()
|
||||
if (logs.size > 120) logs.removeAt(0)
|
||||
logs.add(message)
|
||||
}
|
||||
|
||||
@@ -235,6 +246,10 @@ class PatchSnapchatScreen : SetupScreen() {
|
||||
pushStatus("Patched build ready. Install to finish.")
|
||||
}.onFailure {
|
||||
error = it.message ?: it.toString()
|
||||
it.stackTraceToString()
|
||||
.lineSequence()
|
||||
.filter { line -> line.isNotBlank() }
|
||||
.forEach { line -> pushLog(line) }
|
||||
pushStatus("Failed: ${it.message}")
|
||||
}
|
||||
isRunning = false
|
||||
@@ -273,6 +288,66 @@ class PatchSnapchatScreen : SetupScreen() {
|
||||
)
|
||||
}
|
||||
|
||||
if (showIssuesDialog) {
|
||||
AestheticDialog(
|
||||
onDismissRequest = { showIssuesDialog = false },
|
||||
title = "Facing issues?",
|
||||
text = "",
|
||||
icon = Icons.Filled.Info,
|
||||
confirmButtonText = "Got it",
|
||||
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 = "How to fix installation errors",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "Issue: App cannot be installed because it conflicts with an existing package.",
|
||||
style = bodyStyle,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
Text(
|
||||
text = "Fix: Download Snapchat from the Play Store and uninstall it without keeping data. Run Auto Patcher again. If it still does not work, run:",
|
||||
style = bodyStyle,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
Text(
|
||||
text = "adb uninstall com.snapchat.android",
|
||||
style = bodyStyle,
|
||||
textAlign = TextAlign.Start,
|
||||
softWrap = false,
|
||||
modifier = Modifier.horizontalScroll(rememberScrollState())
|
||||
)
|
||||
Text(
|
||||
text = "Issue: App not installed because the package appears to be invalid.",
|
||||
style = bodyStyle,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
Text(
|
||||
text = "Fix: Download and install JingMatrix LSPatch, then patch Snapchat 13.64.0.52 in Integrated mode. Select Embed Modules and embed the PurrfectSnap APK. Then choose Skip auto setup during PurrfectSnap setup to skip Auto Patcher.",
|
||||
style = bodyStyle,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SetupCard {
|
||||
StepTitle(
|
||||
title = "Auto Patcher",
|
||||
@@ -307,33 +382,48 @@ class PatchSnapchatScreen : SetupScreen() {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val isDownloading = progress >= 0f
|
||||
val isPatching = isRunning && downloadFinished && !isDownloading
|
||||
Text(
|
||||
text = if (progress >= 0f) {
|
||||
"Downloading Snapchat ${(progress * 100).toInt()}%"
|
||||
} else if (isRunning && downloadFinished) {
|
||||
"Patching..."
|
||||
} else {
|
||||
"Initializing..."
|
||||
text = when {
|
||||
isDownloading -> "Downloading Snapchat ${(progress * 100).toInt()}%"
|
||||
isPatching -> "Patching..."
|
||||
else -> "Initializing..."
|
||||
},
|
||||
color = PurrfectPalette.textPrimary,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
LinearProgressIndicator(
|
||||
progress = if (progress >= 0f) progress.coerceIn(0f, 1f) else 0f,
|
||||
color = PurrfectPalette.glowPrimary,
|
||||
trackColor = Color.White.copy(alpha = 0.12f),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
if (isDownloading) {
|
||||
LinearProgressIndicator(
|
||||
progress = { progress.coerceIn(0f, 1f) },
|
||||
color = PurrfectPalette.glowPrimary,
|
||||
trackColor = Color.White.copy(alpha = 0.12f),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
} else {
|
||||
LinearProgressIndicator(
|
||||
color = PurrfectPalette.glowPrimary,
|
||||
trackColor = Color.White.copy(alpha = 0.12f),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogsPanel(
|
||||
logs = logs,
|
||||
pulse = logPulse,
|
||||
accent = accent
|
||||
accent = accent,
|
||||
onCopy = {
|
||||
clipboard.setText(AnnotatedString(logs.joinToString("\n")))
|
||||
pushLog("Logs copied to clipboard.")
|
||||
}
|
||||
)
|
||||
|
||||
error?.let {
|
||||
@@ -389,6 +479,36 @@ class PatchSnapchatScreen : SetupScreen() {
|
||||
onClick = { installPatchedApk() },
|
||||
enabled = true
|
||||
)
|
||||
val issuesInteraction = remember { MutableInteractionSource() }
|
||||
Surface(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = Color.White.copy(alpha = 0.04f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.14f)),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.scaleOnPress(issuesInteraction)
|
||||
.clickable(
|
||||
interactionSource = issuesInteraction,
|
||||
indication = null
|
||||
) { showIssuesDialog = true }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Info,
|
||||
contentDescription = null,
|
||||
tint = Color.White.copy(alpha = 0.9f)
|
||||
)
|
||||
Text(
|
||||
text = "Facing issues?",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
val manualInteraction = remember { MutableInteractionSource() }
|
||||
Surface(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
@@ -520,7 +640,8 @@ private fun GradientActionButton(
|
||||
private fun LogsPanel(
|
||||
logs: List<String>,
|
||||
pulse: Float,
|
||||
accent: Brush
|
||||
accent: Brush,
|
||||
onCopy: () -> Unit
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
val animatedBrush = Brush.linearGradient(
|
||||
@@ -547,21 +668,54 @@ private fun LogsPanel(
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { expanded = !expanded },
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Logs",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Icon(
|
||||
imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
|
||||
contentDescription = null,
|
||||
tint = Color.White
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable { expanded = !expanded },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Logs",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Icon(
|
||||
imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
|
||||
contentDescription = null,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.16f)),
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.clickable { onCopy() }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ContentCopy,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Copy",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
package me.eternal.purrfectsnap.ui.setup.screens.impl
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Verified
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.mutableFloatStateOf
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
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 androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.eternal.purrfectsnap.setup.patch.AutoPatchServer
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.setup.screens.SetupScreen
|
||||
import me.eternal.purrfectsnap.ui.util.scaleOnPress
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
class RootInstallSnapchatScreen : SetupScreen() {
|
||||
private val autoPatchServer = AutoPatchServer()
|
||||
private val okHttpClient = OkHttpClient.Builder()
|
||||
.callTimeout(1, TimeUnit.HOURS)
|
||||
.connectTimeout(1, TimeUnit.HOURS)
|
||||
.readTimeout(1, TimeUnit.HOURS)
|
||||
.writeTimeout(1, TimeUnit.HOURS)
|
||||
.build()
|
||||
private val targetPackage = "com.snapchat.android"
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val logs = remember { mutableStateListOf("Snapchat installer is ready.") }
|
||||
@Suppress("DEPRECATION")
|
||||
val clipboard = LocalClipboardManager.current
|
||||
var progress by remember { mutableFloatStateOf(-1f) }
|
||||
var downloadedApkPath by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val downloadedApk = remember(downloadedApkPath) { downloadedApkPath?.let(::File) }
|
||||
var isRunning by remember { mutableStateOf(false) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
var downloadStartedAt by rememberSaveable { mutableStateOf(0L) }
|
||||
var installVerified by rememberSaveable { mutableStateOf(false) }
|
||||
var installRequested by rememberSaveable { mutableStateOf(false) }
|
||||
var installWatcher by remember { mutableStateOf<Job?>(null) }
|
||||
var downloadFinished by rememberSaveable { mutableStateOf(false) }
|
||||
val logPulse by rememberInfiniteTransition(label = "rootInstallLogPulse").animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
tween(durationMillis = 2200, easing = FastOutSlowInEasing),
|
||||
RepeatMode.Reverse
|
||||
),
|
||||
label = "rootInstallLogPulseValue"
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) { allowNext(false) }
|
||||
LaunchedEffect(installVerified) { allowNext(installVerified) }
|
||||
|
||||
fun pushLog(message: String) {
|
||||
if (logs.size > 120) logs.removeAt(0)
|
||||
logs.add(message)
|
||||
}
|
||||
|
||||
fun isSnapchatInstalledAfter(timestamp: Long): Boolean {
|
||||
if (timestamp == 0L) return false
|
||||
val info = runCatching {
|
||||
context.androidContext.packageManager.getPackageInfo(targetPackage, 0)
|
||||
}.getOrNull() ?: return false
|
||||
return info.lastUpdateTime >= timestamp
|
||||
}
|
||||
|
||||
fun isSnapchatInstalled(): Boolean {
|
||||
return runCatching { context.androidContext.packageManager.getPackageInfo(targetPackage, 0) }.isSuccess
|
||||
}
|
||||
|
||||
fun startInstallWatcher() {
|
||||
if (downloadStartedAt == 0L) return
|
||||
installWatcher?.cancel()
|
||||
installWatcher = coroutineScope.launch {
|
||||
repeat(80) {
|
||||
if (isSnapchatInstalledAfter(downloadStartedAt)) {
|
||||
installVerified = true
|
||||
pushLog("Snapchat install confirmed. You're cleared to continue.")
|
||||
return@launch
|
||||
}
|
||||
delay(1200)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(installRequested, downloadStartedAt) {
|
||||
if (installRequested && downloadStartedAt > 0) {
|
||||
startInstallWatcher()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(downloadStartedAt) {
|
||||
if (downloadStartedAt > 0 && isSnapchatInstalledAfter(downloadStartedAt)) {
|
||||
installVerified = true
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(installVerified) {
|
||||
if (installVerified) {
|
||||
downloadedApk?.let { runCatching { it.delete() } }
|
||||
downloadedApkPath = null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun pushStatus(message: String) = withContext(Dispatchers.Main) { pushLog(message) }
|
||||
suspend fun setProgress(value: Float) = withContext(Dispatchers.Main) { progress = value }
|
||||
|
||||
suspend fun downloadSnapchatFromAutoPatchServer(): File? = withContext(Dispatchers.IO) {
|
||||
val latestApk = autoPatchServer.fetchLatestSnapchatApk() ?: return@withContext null
|
||||
pushStatus("Downloading recommended Snapchat version (${latestApk.tagName})...")
|
||||
|
||||
okHttpClient.newCall(Request.Builder().url(latestApk.downloadUrl).build()).execute().use { response ->
|
||||
if (!response.isSuccessful) return@withContext null
|
||||
val cacheDir = context.activity?.externalCacheDir ?: context.androidContext.externalCacheDir
|
||||
?: context.androidContext.cacheDir
|
||||
val outputFile = File(cacheDir, latestApk.apkName)
|
||||
outputFile.outputStream().use { output ->
|
||||
response.body?.byteStream()?.use { input ->
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var read: Int
|
||||
var totalRead = 0L
|
||||
val totalSize = response.body?.contentLength() ?: -1L
|
||||
while (input.read(buffer).also { read = it } != -1) {
|
||||
output.write(buffer, 0, read)
|
||||
totalRead += read
|
||||
if (totalSize > 0) setProgress(totalRead.toFloat() / totalSize.toFloat())
|
||||
}
|
||||
} ?: return@withContext null
|
||||
}
|
||||
setProgress(-1f)
|
||||
outputFile
|
||||
}
|
||||
}
|
||||
|
||||
fun installDownloadedApk() {
|
||||
val apk = downloadedApk ?: return
|
||||
installRequested = true
|
||||
startInstallWatcher()
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context.androidContext,
|
||||
"${context.androidContext.packageName}.fileprovider",
|
||||
apk
|
||||
)
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
context.androidContext.startActivity(intent)
|
||||
}
|
||||
|
||||
fun markAlreadyInstalled() {
|
||||
installRequested = false
|
||||
installVerified = true
|
||||
pushLog("Marked as installed manually. You're cleared to continue.")
|
||||
}
|
||||
|
||||
fun startDownloadAndInstall() {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
isRunning = true
|
||||
allowNext(false)
|
||||
error = null
|
||||
progress = -1f
|
||||
downloadedApkPath = null
|
||||
installVerified = false
|
||||
installRequested = false
|
||||
downloadFinished = false
|
||||
downloadStartedAt = System.currentTimeMillis()
|
||||
logs.clear()
|
||||
pushLog("Starting Snapchat download for rooted install.")
|
||||
runCatching {
|
||||
if (isSnapchatInstalled()) {
|
||||
pushStatus("Snapchat is installed. Please uninstall it first (don't keep data), then try again.")
|
||||
throw IllegalStateException("Snapchat still installed. Uninstall it first, to continue.")
|
||||
}
|
||||
pushStatus("Fetching recommended Snapchat APK...")
|
||||
val downloaded = downloadSnapchatFromAutoPatchServer()
|
||||
?: throw IllegalStateException("Download failed")
|
||||
downloadedApkPath = downloaded.absolutePath
|
||||
pushStatus("Download completed: ${downloaded.name}")
|
||||
downloadFinished = true
|
||||
pushStatus("Launching installer...")
|
||||
installDownloadedApk()
|
||||
}.onFailure {
|
||||
error = it.message ?: it.toString()
|
||||
it.stackTraceToString()
|
||||
.lineSequence()
|
||||
.filter { line -> line.isNotBlank() }
|
||||
.forEach { line -> pushLog(line) }
|
||||
pushStatus("Failed: ${it.message}")
|
||||
}
|
||||
isRunning = false
|
||||
progress = -1f
|
||||
}
|
||||
}
|
||||
|
||||
val accent = remember {
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowSecondary,
|
||||
PurrfectPalette.glowPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
SetupCard {
|
||||
StepTitle(
|
||||
title = "Snapchat Installer",
|
||||
subtitle = null,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = Color.White.copy(alpha = 0.03f),
|
||||
tonalElevation = 0.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.4f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay)
|
||||
.padding(horizontal = 18.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
AnimatedVisibility(visible = isRunning || progress >= 0f) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val isDownloading = progress >= 0f
|
||||
Text(
|
||||
text = if (isDownloading) {
|
||||
"Downloading Snapchat ${(progress * 100).toInt()}%"
|
||||
} else {
|
||||
"Preparing installer..."
|
||||
},
|
||||
color = PurrfectPalette.textPrimary,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
if (isDownloading) {
|
||||
LinearProgressIndicator(
|
||||
progress = { progress.coerceIn(0f, 1f) },
|
||||
color = PurrfectPalette.glowPrimary,
|
||||
trackColor = Color.White.copy(alpha = 0.12f),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
} else {
|
||||
LinearProgressIndicator(
|
||||
color = PurrfectPalette.glowPrimary,
|
||||
trackColor = Color.White.copy(alpha = 0.12f),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogsPanel(
|
||||
logs = logs,
|
||||
pulse = logPulse,
|
||||
accent = accent,
|
||||
onCopy = {
|
||||
clipboard.setText(AnnotatedString(logs.joinToString("\n")))
|
||||
pushLog("Logs copied to clipboard.")
|
||||
}
|
||||
)
|
||||
|
||||
error?.let {
|
||||
Text(
|
||||
text = it,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (installVerified) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = Color.White.copy(alpha = 0.06f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.18f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = Color.White
|
||||
)
|
||||
Text(
|
||||
text = "Snapchat installed",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (downloadedApk == null) {
|
||||
GradientActionButton(
|
||||
label = "Download Snapchat",
|
||||
icon = Icons.Filled.Download,
|
||||
onClick = { startDownloadAndInstall() },
|
||||
enabled = !isRunning
|
||||
)
|
||||
}
|
||||
if (downloadedApk != null) {
|
||||
GradientActionButton(
|
||||
label = "Install Snapchat",
|
||||
icon = Icons.Filled.Verified,
|
||||
onClick = { installDownloadedApk() },
|
||||
enabled = true
|
||||
)
|
||||
val manualInteraction = remember { MutableInteractionSource() }
|
||||
Surface(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = Color.White.copy(alpha = 0.04f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.14f)),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.scaleOnPress(manualInteraction)
|
||||
.clickable(
|
||||
interactionSource = manualInteraction,
|
||||
indication = null
|
||||
) { markAlreadyInstalled() }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Info,
|
||||
contentDescription = null,
|
||||
tint = Color.White.copy(alpha = 0.9f)
|
||||
)
|
||||
Text(
|
||||
text = "Already Installed?",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GradientActionButton(
|
||||
label: String,
|
||||
icon: ImageVector,
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val interaction = remember { MutableInteractionSource() }
|
||||
val gradient = Brush.horizontalGradient(listOf(PurrfectPalette.glowSecondary, PurrfectPalette.glowPrimary))
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Color.Transparent)
|
||||
.scaleOnPress(interaction)
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
interactionSource = interaction,
|
||||
indication = null,
|
||||
onClick = onClick
|
||||
),
|
||||
tonalElevation = 0.dp,
|
||||
color = Color.Transparent,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.14f))
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(if (enabled) gradient else Brush.horizontalGradient(listOf(Color.White.copy(alpha = 0.08f), Color.White.copy(alpha = 0.08f))))
|
||||
.padding(vertical = 14.dp, horizontal = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = Color.White
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LogsPanel(
|
||||
logs: List<String>,
|
||||
pulse: Float,
|
||||
accent: Brush,
|
||||
onCopy: () -> Unit
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
val animatedBrush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.18f + 0.1f * pulse),
|
||||
Color.Transparent,
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.12f + 0.1f * (1 - pulse))
|
||||
),
|
||||
start = Offset.Zero,
|
||||
end = Offset(400f * (0.6f + pulse), 260f * (0.4f + (1 - pulse)))
|
||||
)
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = Color.White.copy(alpha = 0.03f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(animatedBrush)
|
||||
.background(Color.Black.copy(alpha = 0.25f))
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable { expanded = !expanded },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Logs",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Icon(
|
||||
imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
|
||||
contentDescription = null,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.16f)),
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.clickable { onCopy() }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ContentCopy,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Copy",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
logs.forEach { line ->
|
||||
Text(
|
||||
text = "- $line",
|
||||
color = PurrfectPalette.textPrimary,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,28 @@ class SaveFolderScreen : SetupScreen() {
|
||||
) {
|
||||
Text(text = context.translation["setup.dialogs.select_save_folder_button"])
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
val defaultSrc = remember { MutableInteractionSource() }
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
currentFolder = ""
|
||||
context.config.root.downloader.saveFolder.set("")
|
||||
context.sharedPreferences.edit().putBoolean("downloader_use_default_save_folder", true).apply()
|
||||
context.config.writeConfig()
|
||||
goNext()
|
||||
},
|
||||
interactionSource = defaultSrc,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.scaleOnPress(defaultSrc),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = Color.White
|
||||
),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.18f))
|
||||
) {
|
||||
Text(text = "Use default location")
|
||||
}
|
||||
|
||||
if (showNoPickerDialog) {
|
||||
Dialog(onDismissRequest = { showNoPickerDialog = false }) {
|
||||
|
||||
@@ -40,7 +40,10 @@ class ActivityLauncherHelper(
|
||||
|
||||
fun launch(intent: Intent, callback: ActivityLauncherCallback, onFailure: ((Throwable) -> Unit)?) {
|
||||
if (this.callback != null) {
|
||||
throw IllegalStateException("Already launching an activity")
|
||||
val error = IllegalStateException("Already launching an activity")
|
||||
AbstractLogger.directError("Ignored concurrent activity launch", error)
|
||||
onFailure?.invoke(error)
|
||||
return
|
||||
}
|
||||
this.callback = callback
|
||||
|
||||
@@ -56,7 +59,11 @@ class ActivityLauncherHelper(
|
||||
|
||||
fun requestPermission(permission: String, callback: ActivityLauncherCallback) {
|
||||
if (this.callback != null) {
|
||||
throw IllegalStateException("Already launching an activity")
|
||||
AbstractLogger.directError(
|
||||
"Ignored concurrent permission request",
|
||||
IllegalStateException("Already launching an activity")
|
||||
)
|
||||
return
|
||||
}
|
||||
this.callback = callback
|
||||
permissionResultLauncher.launch(permission)
|
||||
|
||||
@@ -207,23 +207,30 @@ class AlertDialogs(
|
||||
@Composable
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun UniqueSelectionDialog(property: PropertyPair<*>) {
|
||||
val disabledKey = property.key.params.disabledKey
|
||||
val keys = (property.value.defaultValues as List<String>).toMutableList().apply {
|
||||
add(0, "null")
|
||||
val disabledEntry = disabledKey ?: "null"
|
||||
remove(disabledEntry)
|
||||
add(0, disabledEntry)
|
||||
if (disabledKey == null) {
|
||||
remove("null")
|
||||
add(0, "null")
|
||||
}
|
||||
}
|
||||
|
||||
var selectedValue by remember {
|
||||
mutableStateOf(property.value.getNullable()?.toString() ?: "null")
|
||||
mutableStateOf(property.value.getNullable()?.toString() ?: (disabledKey ?: "null"))
|
||||
}
|
||||
|
||||
DefaultDialogCard {
|
||||
keys.forEachIndexed { index, item ->
|
||||
fun select() {
|
||||
selectedValue = item
|
||||
property.value.setAny(if (index == 0) {
|
||||
null
|
||||
} else {
|
||||
item
|
||||
})
|
||||
if (disabledKey != null && item == disabledKey) {
|
||||
property.value.setAny(disabledKey)
|
||||
return
|
||||
}
|
||||
property.value.setAny(if (disabledKey == null && index == 0) null else item)
|
||||
}
|
||||
|
||||
Row(
|
||||
@@ -279,6 +286,7 @@ class AlertDialogs(
|
||||
else -> KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
},
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.White.copy(alpha = 0.08f),
|
||||
unfocusedContainerColor = Color.White.copy(alpha = 0.05f),
|
||||
@@ -362,6 +370,7 @@ class AlertDialogs(
|
||||
fieldValue.value = it
|
||||
},
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.White.copy(alpha = 0.08f),
|
||||
unfocusedContainerColor = Color.White.copy(alpha = 0.05f),
|
||||
|
||||
BIN
app/src/main/res/drawable/pfp_external.jpg
Normal file
BIN
app/src/main/res/drawable/pfp_external.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
BIN
app/src/main/res/drawable/pfp_rsr.jpg
Normal file
BIN
app/src/main/res/drawable/pfp_rsr.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="cache" path="." />
|
||||
<external-cache-path name="external_cache" path="." />
|
||||
</paths>
|
||||
|
||||
@@ -33,8 +33,8 @@ tasks.register<GetVersionTask>("getVersion") {
|
||||
}
|
||||
|
||||
// You can still set these for legacy use by submodules or scripts:
|
||||
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.0.0").get())
|
||||
rootProject.ext.set("appVersionCode", 210)
|
||||
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.1.0").get())
|
||||
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("1").get().toInt())
|
||||
rootProject.ext.set("applicationId", "me.eternal.purrfectsnap")
|
||||
rootProject.ext.set(
|
||||
"buildHash",
|
||||
|
||||
@@ -1,5 +1,38 @@
|
||||
## v1.1.0
|
||||
- test4
|
||||
- Fix: message preview overlap in French.
|
||||
- Fix: transparent gallery media send override dialog.
|
||||
- Fix: message logger autopurge "Never" no longer resets to 3 days.
|
||||
- Fix: crash during auto patching on certain older Android versions.
|
||||
- Fix: audio mismatch.
|
||||
- Fix: app lock crash.
|
||||
- New: translations added.
|
||||
- Fix: message list no longer autoscrolls.
|
||||
- New: included an Easter egg.
|
||||
- Fix: disable Bitmoji feature.
|
||||
- UI: various UI elements fixed.
|
||||
- UI: add friends dialog now includes an "Unselect all" button.
|
||||
- UI: redesigned remaining UI elements.
|
||||
- New: select folder screen has "Use default" option for cloned installs.
|
||||
- Fix: export memories crash on certain devices.
|
||||
- Fix: config state persistence on certain devices.
|
||||
- Fix: notification crash on certain devices.
|
||||
- Fix: friend suggestions (again).
|
||||
- Change: removed pull down gesture to open menu; use Quick Add instead.
|
||||
- New: custom color feature for export chat list.
|
||||
- New: exported chat messages now include message logger.
|
||||
- Fix: unsaveable snaps.
|
||||
- UI: many UI elements made symmetrical.
|
||||
- UI: redesigned file imports screen.
|
||||
- Fix: config.json and update frequency dialogs now open on click.
|
||||
- Fix: bypass camera roll limit.
|
||||
- Fix: "no friends found" text now visible in bulk messaging action.
|
||||
- Fix: search results list no longer autoscrolls while typing.
|
||||
- New: in-app guides for rooted and non-rooted devices plus a troubleshooting guide.
|
||||
- New: Snapchat auto downloader for rooted devices.
|
||||
- Fix: quick tiles appearance.
|
||||
- Fix: dialog position in Friend Tracker sort dialog.
|
||||
- New: about section added.
|
||||
- Improvement: optimizations throughout the app.
|
||||
|
||||
## v1.0.0
|
||||
- test3
|
||||
- Test
|
||||
|
||||
@@ -1,5 +1,38 @@
|
||||
## v1.1.0
|
||||
- test2
|
||||
- Fix: message preview overlap in French.
|
||||
- Fix: transparent gallery media send override dialog.
|
||||
- Fix: message logger autopurge "Never" no longer resets to 3 days.
|
||||
- Fix: crash during auto patching on certain older Android versions.
|
||||
- Fix: audio mismatch.
|
||||
- Fix: app lock crash.
|
||||
- New: translations added.
|
||||
- Fix: message list no longer autoscrolls.
|
||||
- New: included an Easter egg.
|
||||
- Fix: disable Bitmoji feature.
|
||||
- UI: various UI elements fixed.
|
||||
- UI: add friends dialog now includes an "Unselect all" button.
|
||||
- UI: redesigned remaining UI elements.
|
||||
- New: select folder screen has "Use default" option for cloned installs.
|
||||
- Fix: export memories crash on certain devices.
|
||||
- Fix: config state persistence on certain devices.
|
||||
- Fix: notification crash on certain devices.
|
||||
- Fix: friend suggestions (again).
|
||||
- Change: removed pull down gesture to open menu; use Quick Add instead.
|
||||
- New: custom color feature for export chat list.
|
||||
- New: exported chat messages now include message logger.
|
||||
- Fix: unsaveable snaps.
|
||||
- UI: many UI elements made symmetrical.
|
||||
- UI: redesigned file imports screen.
|
||||
- Fix: config.json and update frequency dialogs now open on click.
|
||||
- Fix: bypass camera roll limit.
|
||||
- Fix: "no friends found" text now visible in bulk messaging action.
|
||||
- Fix: search results list no longer autoscrolls while typing.
|
||||
- New: in-app guides for rooted and non-rooted devices plus a troubleshooting guide.
|
||||
- New: Snapchat auto downloader for rooted devices.
|
||||
- Fix: quick tiles appearance.
|
||||
- Fix: dialog position in Friend Tracker sort dialog.
|
||||
- New: about section added.
|
||||
- Improvement: optimizations throughout the app.
|
||||
|
||||
## v1.0.0
|
||||
- test 1
|
||||
- Test.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Vælg sprog",
|
||||
@@ -42,7 +42,7 @@
|
||||
"social": "Sociale",
|
||||
"manage_scope": "Håndtér anvendelsesområde",
|
||||
"messaging_preview": "Forhåndsvisning",
|
||||
"scripts": "Scripts",
|
||||
"scripts": "Skripter",
|
||||
"manage_script_repos": "Håndtér script- referater",
|
||||
"view_logger_history": "Loggerhistorik",
|
||||
"better_location": "Bedre placering"
|
||||
@@ -60,7 +60,7 @@
|
||||
"version_title": "v{versionName}· af den evige",
|
||||
"update_title": "PurrfectSnap opdatering",
|
||||
"update_content": "Version{version}er tilgængelig!",
|
||||
"update_button": "Download",
|
||||
"update_button": "Hent",
|
||||
"debug_build_summary_title": "Du kører en debug build af PurrfectSnap",
|
||||
"debug_build_summary_content": "Version{versionName}({versionCode})",
|
||||
"debug_build_summary_date": "Byggedato:{date}({days}dage siden)",
|
||||
@@ -84,7 +84,7 @@
|
||||
"clear_button": "Ryd",
|
||||
"view_logger_history_button": "Vis loggerhistorik",
|
||||
"ui_settings_title": "Brugerindstillinger",
|
||||
"haptic_feedback_label": "Haptic Feedback",
|
||||
"haptic_feedback_label": "Haptisk feedback",
|
||||
"use_system_toasts_label": "Brug systemskåle",
|
||||
"updates_title": "Opdateringer",
|
||||
"auto_update_check": "Automatisk opdateringstjek",
|
||||
@@ -107,7 +107,7 @@
|
||||
"friend_notes_no_notes_to_backup": "Ingen noter at bakke op endnu",
|
||||
"friend_notes_backup_success": "Ven noter bakkes op",
|
||||
"friend_notes_restore_success": "Ven noter genoprettet",
|
||||
"backup_button": "Backup",
|
||||
"backup_button": "Sikkerhedskopi",
|
||||
"restore_button": "Gendan",
|
||||
"view_button": "Vis",
|
||||
"customize_bottom_bar_title": "Tilpas underlinjen",
|
||||
@@ -126,10 +126,10 @@
|
||||
"merge_button": "Sammenflet",
|
||||
"failed_to_open_file": "Kunne ikke åbne filen",
|
||||
"merge_files_toast": "Sammenfletning{count}filer",
|
||||
"remove_selected_tasks_title": "Er du sikker på du ønsker at fjerne valgte opgaver?",
|
||||
"remove_all_tasks_title": "Er du sikker på, du ønsker at fjerne alle opgaver?",
|
||||
"remove_selected_tasks_title": "Er du sikker p?, at du vil fjerne de valgte opgaver?",
|
||||
"remove_all_tasks_title": "Er du sikker p?, at du vil fjerne alle opgaver?",
|
||||
"delete_files_option": "Slet også filer",
|
||||
"remove_selected_tasks_confirm": "Fjern{count}opgaver?",
|
||||
"remove_selected_tasks_confirm": "Fjern {count} opgaver?",
|
||||
"remove_all_tasks_confirm": "Fjern alle opgaver?"
|
||||
},
|
||||
"features": {
|
||||
@@ -187,7 +187,7 @@
|
||||
"streaks_expiration_text": "Udløber{eta}",
|
||||
"streaks_expiration_text_expired": "Udløb",
|
||||
"reminder_button": "Sæt påmindelse",
|
||||
"delete_scope_confirm_dialog_title": "Er du sikker på, at du ønsker at slette en{scope}?",
|
||||
"delete_scope_confirm_dialog_title": "Er du sikker p?, at du ?nsker at slette en {scope}?",
|
||||
"notes_placeholder": "Klik for at tilføje en note"
|
||||
},
|
||||
"logged_stories": {
|
||||
@@ -305,7 +305,7 @@
|
||||
"clear_module_data_failed": "Kunne ikke rydde moduldata",
|
||||
"delete_module_button": "Slet",
|
||||
"delete_module_failed": "Kunne ikke slette modulet",
|
||||
"documentation_button": "Docs",
|
||||
"documentation_button": "Dokumentation",
|
||||
"download_script_failed": "Kunne ikke downloade script",
|
||||
"downloading_script": "Downloader script...",
|
||||
"edit_module_button": "Redigér",
|
||||
@@ -341,7 +341,7 @@
|
||||
"no_scripts_folder_selected": "Vælg en scripts mappe først",
|
||||
"no_scripts_available": "Ingen scripts tilgængelige",
|
||||
"installed_button": "Installeret",
|
||||
"download_button": "Download"
|
||||
"download_button": "Hent"
|
||||
},
|
||||
"repos": {
|
||||
"no_repos_added": "Ingen datalagre tilføjet",
|
||||
@@ -407,7 +407,7 @@
|
||||
"duplicate_rule_name_dialog_title": "Duplikeret regelnavn",
|
||||
"duplicate_rule_name_dialog_text": "Der findes allerede en regel med dette navn. Vælg et nyt navn.",
|
||||
"discard_changes_dialog_title": "Kassér ændringer?",
|
||||
"discard_changes_dialog_text": "Du har ikke gemt ændringer. Kassere dem?",
|
||||
"discard_changes_dialog_text": "Du har ikke gemt ?ndringer. Kass?r dem?",
|
||||
"rule_subtitle": "Indstil udløsere og skoper til denne regel.",
|
||||
"discard_button": "Kassér",
|
||||
"enabled_label": "Aktiveret",
|
||||
@@ -416,7 +416,7 @@
|
||||
"delete_rule_dialog_text": "Er du sikker på, du vil slette denne regel?",
|
||||
"no_repos_added": "Ingen datalagre tilføjet",
|
||||
"import_dialog_title": "Importregler",
|
||||
"bulk_import_button": "Bulk import",
|
||||
"bulk_import_button": "Masseimport",
|
||||
"individual_import_button": "Engangsimport",
|
||||
"invalid_import_type_dialog_title": "Ugyldig import",
|
||||
"invalid_import_type_dialog_text": "Den valgte filtype matcher ikke importtilstanden.",
|
||||
@@ -546,7 +546,7 @@
|
||||
"name": "Brug E2E kryptering"
|
||||
},
|
||||
"pin_conversation": {
|
||||
"name": "Pin Conversation"
|
||||
"name": "Fastg?r samtale"
|
||||
},
|
||||
"exclude_message_logger": {
|
||||
"name": "Ekskl. fra brevlogger"
|
||||
@@ -615,7 +615,7 @@
|
||||
"description": "Udfører operationer såsom sletning af venner eller masse sletning af samtaler"
|
||||
},
|
||||
"regen_mappings": {
|
||||
"name": "Regenerate Mappings",
|
||||
"name": "Gendan mappinger",
|
||||
"description": "Manuelt regenerere tilknytninger"
|
||||
},
|
||||
"change_language": {
|
||||
@@ -690,7 +690,7 @@
|
||||
},
|
||||
"settings_menu": {
|
||||
"default": "Standard",
|
||||
"legacy": "Legacy"
|
||||
"legacy": "Klassisk"
|
||||
},
|
||||
"path_format": {
|
||||
"create_author_folder": "Opret mappe for hver forfatter",
|
||||
@@ -708,7 +708,7 @@
|
||||
"spotlight": "Spotlight"
|
||||
},
|
||||
"logging": {
|
||||
"started": "Started",
|
||||
"started": "Startet",
|
||||
"success": "Succes",
|
||||
"progress": "Fremskridt",
|
||||
"failure": "Fejl"
|
||||
@@ -716,7 +716,7 @@
|
||||
"notifications": {
|
||||
"chat_screenshot": "Skærmbillede",
|
||||
"chat_screen_record": "Skærmoptegnelse",
|
||||
"snap_replay": "Snap Replay",
|
||||
"snap_replay": "Snap-genafspilning",
|
||||
"camera_roll_save": "Gem kamerarulle",
|
||||
"chat": "Chat",
|
||||
"chat_reply": "Chat svar",
|
||||
@@ -843,7 +843,7 @@
|
||||
"ORIGINAL": "Originale medier",
|
||||
"NOTE": "Lydnote",
|
||||
"SNAP": "Snap",
|
||||
"SAVEABLE_SNAP": "Saveable Snap",
|
||||
"SAVEABLE_SNAP": "Snap der kan gemmes",
|
||||
"null": "Snapchat- standard"
|
||||
},
|
||||
"strip_media_metadata": {
|
||||
@@ -888,12 +888,12 @@
|
||||
"null": "Automatisk"
|
||||
},
|
||||
"update_check_frequency": {
|
||||
"null": "Auto"
|
||||
"null": "Automatisk"
|
||||
},
|
||||
"snapchat_plus": {
|
||||
"not_subscribed": "Ikke indskrevet",
|
||||
"basic": "Grundlæggende",
|
||||
"ad_free": "Ad Free",
|
||||
"ad_free": "Reklamefri",
|
||||
"null": "Standard"
|
||||
},
|
||||
"bypass_video_length_restriction": {
|
||||
@@ -1016,19 +1016,19 @@
|
||||
"friendly, casual, helpful, empathetic": "venlig, afslappet, hjælpsom, empatisk"
|
||||
},
|
||||
"ai_response_style": {
|
||||
"casual": "Casual",
|
||||
"casual": "Uformel",
|
||||
"formal": "Formel",
|
||||
"friendly": "Venlig",
|
||||
"humorous": "Humoristisk",
|
||||
"empathetic": "Empatic",
|
||||
"toxic": "Edgy",
|
||||
"toxic": "Kantet",
|
||||
"busy": "Travl"
|
||||
},
|
||||
"ai_temperature": {
|
||||
"0.7": "Afbalanceret (0,7)"
|
||||
},
|
||||
"ai_response_language": {
|
||||
"auto": "Auto",
|
||||
"auto": "Automatisk",
|
||||
"en": "Engelsk",
|
||||
"es": "Spansk",
|
||||
"fr": "Fransk",
|
||||
@@ -1112,7 +1112,7 @@
|
||||
"translation_position": {
|
||||
"above": "Ovenfor tekst",
|
||||
"below": "Nedenfor tekst",
|
||||
"inline": "Inline"
|
||||
"inline": "Indlejret"
|
||||
},
|
||||
"source_language": {
|
||||
"auto": "Detektér automatisk"
|
||||
@@ -1139,7 +1139,7 @@
|
||||
"description": "Indstil koordinaterne for den spoofed placering"
|
||||
},
|
||||
"walk_radius": {
|
||||
"name": "Walk Radius",
|
||||
"name": "Gangradius",
|
||||
"description": "Tilfældigt gå rundt inden for denne radius (ft)"
|
||||
},
|
||||
"always_update_location": {
|
||||
@@ -1191,7 +1191,7 @@
|
||||
"description": "Bekræfter automatisk udvalgte handlinger"
|
||||
},
|
||||
"auto_updater": {
|
||||
"name": "Auto Updater",
|
||||
"name": "Auto-opdatering",
|
||||
"description": "Kontrollerer automatisk for nye opdateringer"
|
||||
},
|
||||
"update_settings": {
|
||||
@@ -1210,7 +1210,7 @@
|
||||
"name": "Brugerindstillinger",
|
||||
"properties": {
|
||||
"haptic_feedback": {
|
||||
"name": "Haptic Feedback"
|
||||
"name": "Haptisk feedback"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1282,7 +1282,7 @@
|
||||
},
|
||||
"downloader": {
|
||||
"name": "Downloader",
|
||||
"description": "Download Snapchat Media",
|
||||
"description": "Hent Snapchat-medier",
|
||||
"properties": {
|
||||
"save_folder": {
|
||||
"name": "Gem mappe",
|
||||
@@ -1399,7 +1399,7 @@
|
||||
"description": "Viser en lille forhåndsvisning ved siden af usete snaps i chat"
|
||||
},
|
||||
"bootstrap_override": {
|
||||
"name": "Bootstrap Override",
|
||||
"name": "Bootstrap-override",
|
||||
"description": "Overfører indstillinger for brugergrænseflade bootstrap",
|
||||
"properties": {
|
||||
"app_appearance": {
|
||||
@@ -1445,7 +1445,7 @@
|
||||
"description": "Vælg hvilke UI- komponenter der skal skjules"
|
||||
},
|
||||
"opera_media_quick_info": {
|
||||
"name": "Opera Media Quick Info",
|
||||
"name": "Opera Media hurtiginfo",
|
||||
"description": "Viser nyttig information om medier såsom oprettelsesdato i opera viewers kontekstmenu"
|
||||
},
|
||||
"old_bitmoji_selfie": {
|
||||
@@ -1503,7 +1503,7 @@
|
||||
"description": "Ændre hvordan du interagerer med venner",
|
||||
"properties": {
|
||||
"bypass_screenshot_detection": {
|
||||
"name": "Bypass Screenshot Detection",
|
||||
"name": "Omg? sk?rmbilleddetektering",
|
||||
"description": "Forhindrer Snapchat i at opdage når du tager et screenshot"
|
||||
},
|
||||
"anonymous_story_viewing": {
|
||||
@@ -1593,7 +1593,7 @@
|
||||
"description": "Gruppemeddelelser til en enkelt"
|
||||
},
|
||||
"chat_preview": {
|
||||
"name": "Chat Preview",
|
||||
"name": "Chat-forh?ndsvisning",
|
||||
"description": "Viser en forhåndsvisning af modtagne meddelelser i anmeldelsen"
|
||||
},
|
||||
"media_preview": {
|
||||
@@ -1765,7 +1765,7 @@
|
||||
"description": "Vælg hvilken AI-tjeneste der skal bruges til at generere svar"
|
||||
},
|
||||
"ai_endpoint_url": {
|
||||
"name": "AI Endpoint URL",
|
||||
"name": "AI-endepunkt-URL",
|
||||
"description": "API endpoint URL for AI-tjenesten (fx, OpenAI, lokal AI-server)"
|
||||
},
|
||||
"ai_model": {
|
||||
@@ -1781,7 +1781,7 @@
|
||||
"description": "System prompt, der definerer AI 's personlighed og adfærd"
|
||||
},
|
||||
"ai_max_tokens": {
|
||||
"name": "AI Max Tokens",
|
||||
"name": "AI maks. tokens",
|
||||
"description": "Maksimalt antal tokens (ord) AI kan bruge i svar"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1817,7 +1817,7 @@
|
||||
"description": "Brug skabelonbreve hvis AI ikke genererer et svar"
|
||||
},
|
||||
"ai_request_timeout": {
|
||||
"name": "AI Request Timeout",
|
||||
"name": "AI-foresp?rgselstimeout",
|
||||
"description": "Maksimal tid til at vente på AI respons (i sekunder)"
|
||||
},
|
||||
"ai_retry_attempts": {
|
||||
@@ -1899,7 +1899,7 @@
|
||||
"description": "Mindste forsinkelse i millisekunder før et snap åbnes"
|
||||
},
|
||||
"max_delay_ms": {
|
||||
"name": "Max Delay (ms)",
|
||||
"name": "Maks. forsinkelse (ms)",
|
||||
"description": "Maksimal forsinkelse i millisekunder før et snap åbnes"
|
||||
},
|
||||
"queue_size": {
|
||||
@@ -2090,7 +2090,7 @@
|
||||
}
|
||||
},
|
||||
"streaks_reminder": {
|
||||
"name": "Streaks Reminder",
|
||||
"name": "Streaks-p?mindelse",
|
||||
"description": "Periodisk fortæller dig om dine Streaks",
|
||||
"properties": {
|
||||
"interval": {
|
||||
@@ -2124,11 +2124,11 @@
|
||||
"description": "Viser det første oprettede brugernavn ved siden af det aktuelle brugernavn i profilsiden"
|
||||
},
|
||||
"bypass_camera_roll_limit": {
|
||||
"name": "Bypass Camera Roll Limit",
|
||||
"name": "Omg? kamerarullegr?nse",
|
||||
"description": "Øger den maksimale mængde medier, du kan sende fra kameraet roll"
|
||||
},
|
||||
"custom_self_destruct_snap_delay": {
|
||||
"name": "Custom Self Destruct Snap Delay",
|
||||
"name": "Brugerdefineret selvdestruktionsforsinkelse for Snap",
|
||||
"description": "Giver flere muligheder for selvdestruere timer, når du sender en Snap"
|
||||
},
|
||||
"composer_console": {
|
||||
@@ -2156,7 +2156,7 @@
|
||||
}
|
||||
},
|
||||
"spoof": {
|
||||
"name": "Spoof",
|
||||
"name": "Spoofing",
|
||||
"description": "Spoof forskellige oplysninger om dig",
|
||||
"properties": {
|
||||
"play_store_installer_package_name": {
|
||||
@@ -2208,7 +2208,7 @@
|
||||
"description": "Giver dig mulighed for at vælge en video / lydfil fra galleriet"
|
||||
},
|
||||
"story_logger": {
|
||||
"name": "Story Logger",
|
||||
"name": "Story-logger",
|
||||
"description": "Giver en historie om venner historier"
|
||||
},
|
||||
"call_recorder": {
|
||||
@@ -2244,7 +2244,7 @@
|
||||
}
|
||||
},
|
||||
"voice_note_auto_play": {
|
||||
"name": "Voice Note Auto Play",
|
||||
"name": "Automatisk afspilning af talenoter",
|
||||
"description": "Afspiller automatisk den næste stemme note efter den nuværende er færdig"
|
||||
},
|
||||
"friend_notes": {
|
||||
@@ -2260,7 +2260,7 @@
|
||||
"description": "Forsøg at reparere Venne Feed Menuen som når enheden er offline kan den ikke vises korrekt"
|
||||
},
|
||||
"app_lock": {
|
||||
"name": "App Lock",
|
||||
"name": "Appl?s",
|
||||
"description": "Forhindrer adgang til Snapchat uden adgangskode",
|
||||
"properties": {
|
||||
"lock_on_resume": {
|
||||
@@ -2270,7 +2270,7 @@
|
||||
}
|
||||
},
|
||||
"infinite_story_boost": {
|
||||
"name": "Infinite Story Boost",
|
||||
"name": "Uendelig Story-boost",
|
||||
"description": "Bypass Story Boost Limit forsinkelse"
|
||||
},
|
||||
"meo_passcode_bypass": {
|
||||
@@ -2399,7 +2399,7 @@
|
||||
"LIVE_LOCATION_SHARE": "Live Placering Del",
|
||||
"CREATIVE_TOOL_ITEM": "Kreativt værktøjs element",
|
||||
"FAMILY_CENTER_INVITE": "Invitation til familiecenter",
|
||||
"FAMILY_CENTER_ACCEPT": "Family Center Accept",
|
||||
"FAMILY_CENTER_ACCEPT": "Family Center accept",
|
||||
"FAMILY_CENTER_LEAVE": "Familiecenterorlov",
|
||||
"STATUS_PLUS_GIFT": "Status plus gave",
|
||||
"TINY_SNAP": "Lille snap",
|
||||
@@ -2419,19 +2419,19 @@
|
||||
"media_download_source": {
|
||||
"none": "Ingen",
|
||||
"pending": "Afventer",
|
||||
"chat_media": "Chat Media",
|
||||
"chat_media": "Chatmedier",
|
||||
"story": "Historie",
|
||||
"public_story": "Offentlig historie",
|
||||
"spotlight": "Spotlight",
|
||||
"profile_picture": "Profilbillede",
|
||||
"story_logger": "Story Logger",
|
||||
"story_logger": "Story-logger",
|
||||
"message_logger": "BrevloggerComment",
|
||||
"merged": "Sammenflettet",
|
||||
"voice_call": "Stemmeopkald"
|
||||
},
|
||||
"chat_action_menu": {
|
||||
"preview_button": "Forhåndsvisning",
|
||||
"download_button": "Download",
|
||||
"download_button": "Hent",
|
||||
"delete_logged_message_button": "Slet indlæst brev",
|
||||
"show_chat_edit_history": "Vis Chat Redigér historik",
|
||||
"convert_message": "Konvertér brev"
|
||||
@@ -2454,7 +2454,7 @@
|
||||
"ORIGINAL": "Originale medier",
|
||||
"NOTE": "Lydnote",
|
||||
"SNAP": "Snap",
|
||||
"SAVEABLE_SNAP": "Saveable Snap",
|
||||
"SAVEABLE_SNAP": "Snap der kan gemmes",
|
||||
"null": "Snapchat- standard",
|
||||
"multiple_media_toast": "Du kan kun sende et medie ad gangen"
|
||||
},
|
||||
@@ -2597,7 +2597,7 @@
|
||||
"cancel": "Annullér",
|
||||
"save": "Gem",
|
||||
"open": "Åbn",
|
||||
"download": "Download",
|
||||
"download": "Hent",
|
||||
"send": "Send",
|
||||
"restore_original": "Gendan original",
|
||||
"convert_external_media": "Konverter eksterne medier"
|
||||
@@ -2622,7 +2622,7 @@
|
||||
"snap_replayed": "Snap genspillet",
|
||||
"snap_replayed_twice": "Snap genspillet to gange",
|
||||
"snap_screenshot": "Snap screenshot",
|
||||
"snap_screen_record": "Snap Screen Record",
|
||||
"snap_screen_record": "Snap-sk?rmoptagelse",
|
||||
"i_can_see_you": "Jeg kan se dig"
|
||||
},
|
||||
"cleared_from_feed": "Renset fra foder",
|
||||
@@ -2635,7 +2635,7 @@
|
||||
"better_notifications": {
|
||||
"button": {
|
||||
"reply": "Svar",
|
||||
"download": "Download",
|
||||
"download": "Hent",
|
||||
"mark_as_read": "Markér som læst"
|
||||
}
|
||||
},
|
||||
@@ -2883,7 +2883,7 @@
|
||||
"translation_position": {
|
||||
"above": "Over",
|
||||
"below": "Nedenfor",
|
||||
"inline": "Inline"
|
||||
"inline": "Indlejret"
|
||||
},
|
||||
"language_codes": {
|
||||
"en": "Engelsk",
|
||||
@@ -2984,7 +2984,7 @@
|
||||
},
|
||||
"delete_dialog": {
|
||||
"title": "Slet log",
|
||||
"message": "Er du sikker på, du vil slette alle logfiler? Denne handling kan ikke bringes til ophør.",
|
||||
"message": "Er du sikker p?, du vil slette alle logfiler? Denne handling kan ikke fortrydes.",
|
||||
"confirm": "Slet alle",
|
||||
"cancel": "Annullér",
|
||||
"progress": "Sletning{count}logfiler..."
|
||||
@@ -3064,7 +3064,7 @@
|
||||
}
|
||||
},
|
||||
"ui_settings_title": "Brugerindstillinger",
|
||||
"haptic_feedback_label": "Haptic Feedback",
|
||||
"haptic_feedback_label": "Haptisk feedback",
|
||||
"updates_title": "Opdateringer",
|
||||
"auto_update_check": "Automatisk opdateringstjek",
|
||||
"update_check_frequency_daily": "Daglig",
|
||||
@@ -3156,9 +3156,9 @@
|
||||
"created_at": "Oprettet ved{date}",
|
||||
"failed_to_open_file": "Kunne ikke åbne filen. Tjek logfiler for mere info",
|
||||
"failed_to_get_file": "Kunne ikke få filen",
|
||||
"download_button": "Download",
|
||||
"download_button": "Hent",
|
||||
"chat_attachment": "Bilag{index}",
|
||||
"log_header_format": "{username}?{type}?{date}",
|
||||
"log_header_format": "{username} ? {type} ? {date}",
|
||||
"edited_at_text": "Edited to \"{message}\"på{date}",
|
||||
"list_group_format": "Gruppe{name}",
|
||||
"list_friend_format": "Ven{name}",
|
||||
@@ -3182,7 +3182,7 @@
|
||||
"failed_to_edit_message": "Kunne ikke redigere brev:{error}"
|
||||
},
|
||||
"ai_response_style": {
|
||||
"casual": "Casual",
|
||||
"casual": "Uformel",
|
||||
"formal": "Formel",
|
||||
"friendly": "Venlig",
|
||||
"humorous": "Humoristisk",
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Sprache auswählen",
|
||||
@@ -27,10 +27,10 @@
|
||||
"tasks": "Aufgaben",
|
||||
"features": "Eigenschaften",
|
||||
"manage_rule_feature": "Regelmerkmal verwalten",
|
||||
"home": "Home",
|
||||
"home": "Startseite",
|
||||
"home_about": "Über uns",
|
||||
"home_settings": "Einstellungen",
|
||||
"home_logs": "Logs",
|
||||
"home_logs": "Protokolle",
|
||||
"logger_history": "Geschichte von Logger",
|
||||
"logged_stories": "Geloggte Geschichten",
|
||||
"friend_tracker": "Fröhlicher Tracker",
|
||||
@@ -58,7 +58,7 @@
|
||||
"sections": {
|
||||
"home": {
|
||||
"version_title": "vgl{versionName}· von Eternal",
|
||||
"update_title": "PurrfectSnap Update",
|
||||
"update_title": "PurrfectSnap-Update",
|
||||
"update_content": "Version{version}ist verfügbar!",
|
||||
"update_button": "Downloads",
|
||||
"debug_build_summary_title": "Sie führen einen Debug-Bau von PurrfectSnap",
|
||||
@@ -107,8 +107,8 @@
|
||||
"friend_notes_no_notes_to_backup": "Noch keine Notizen",
|
||||
"friend_notes_backup_success": "Freundschaftsnoten gesichert",
|
||||
"friend_notes_restore_success": "Freundschaftsnoten restauriert",
|
||||
"backup_button": "Backup",
|
||||
"restore_button": "Restore",
|
||||
"backup_button": "Sicherung",
|
||||
"restore_button": "Wiederherstellen",
|
||||
"view_button": "Blick",
|
||||
"customize_bottom_bar_title": "Anpassen Bottom Bar",
|
||||
"customize_bottom_bar_subtitle": "Wählen Sie, welche Tabs auf Ihrem Heimbildschirm anzeigen",
|
||||
@@ -116,7 +116,7 @@
|
||||
"reset_button": "Zurück zur Übersicht",
|
||||
"done_button": "Artikel 2",
|
||||
"clear_friend_feed": "Klarer Friend Feed",
|
||||
"test_mode_label": "Enable PurrAura",
|
||||
"test_mode_label": "PurrAura aktivieren",
|
||||
"disable_feature_loading_label": "Deaktivieren der Funktion",
|
||||
"disable_auto_mapper_label": "Auto Mapper deaktivieren",
|
||||
"disable_bypass_indicator_label": "Bypass Indikator deaktivieren"
|
||||
@@ -126,10 +126,10 @@
|
||||
"merge_button": "Verschmelzung",
|
||||
"failed_to_open_file": "Datei nicht öffnen",
|
||||
"merge_files_toast": "Zusammenführen{count}dateien",
|
||||
"remove_selected_tasks_title": "Sind Sie sicher, dass Sie ausgewählte Aufgaben entfernen möchten?",
|
||||
"remove_all_tasks_title": "Sind Sie sicher, dass Sie alle Aufgaben entfernen möchten?",
|
||||
"remove_selected_tasks_title": "Sind Sie sicher, dass Sie die ausgew?hlten Aufgaben entfernen m?chten?",
|
||||
"remove_all_tasks_title": "Sind Sie sicher, dass Sie alle Aufgaben entfernen m?chten?",
|
||||
"delete_files_option": "Löschen von Dateien",
|
||||
"remove_selected_tasks_confirm": "Entfernen{count}aufgaben?",
|
||||
"remove_selected_tasks_confirm": "{count} Aufgaben entfernen?",
|
||||
"remove_all_tasks_confirm": "Alle Aufgaben entfernen?"
|
||||
},
|
||||
"features": {
|
||||
@@ -175,7 +175,7 @@
|
||||
"e2ee_title": "End-to-End-Verschlüsselung",
|
||||
"e2ee_subtitle": "Verwalten Sie Ihren gemeinsamen Schlüssel für diesen Freund.",
|
||||
"export_base64_button": "Ausfuhrbasis64",
|
||||
"import_base64_button": "Import Base64",
|
||||
"import_base64_button": "Base64 importieren",
|
||||
"invalid_key_size_32_bytes": "Ungültige Schlüsselgröße. Geben Sie einen 32-byte Schlüssel.",
|
||||
"successfully_imported_key": "Schlüssel importiert erfolgreich.",
|
||||
"failed_to_import_key": "Schlüssel nicht importiert:{message}",
|
||||
@@ -187,7 +187,7 @@
|
||||
"streaks_expiration_text": "Abzüge{eta}",
|
||||
"streaks_expiration_text_expired": "Ausgenommen",
|
||||
"reminder_button": "Zurück zur Übersicht",
|
||||
"delete_scope_confirm_dialog_title": "Sind Sie sicher, dass Sie eine löschen möchten{scope}?",
|
||||
"delete_scope_confirm_dialog_title": "Sind Sie sicher, dass Sie {scope} l?schen m?chten?",
|
||||
"notes_placeholder": "Klicken Sie auf eine Notiz hinzufügen"
|
||||
},
|
||||
"logged_stories": {
|
||||
@@ -206,7 +206,7 @@
|
||||
"save_selection_option": "Auswahl speichern",
|
||||
"save_all_option": "Alle speichern",
|
||||
"unsave_selection_option": "Auswahl nicht speichern",
|
||||
"unsave_all_option": "Unsave All",
|
||||
"unsave_all_option": "Alles nicht speichern",
|
||||
"mark_selection_as_seen_option": "Mark ausgewählt Snap als gesehen",
|
||||
"mark_all_as_seen_option": "Markieren Sie alle Snaps wie gesehen",
|
||||
"delete_selection_option": "Auswahl löschen",
|
||||
@@ -220,7 +220,7 @@
|
||||
"list_friend_format": "Freund{name}",
|
||||
"list_group_format": "Gruppe{name}",
|
||||
"no_more_messages": "Keine Nachrichten mehr",
|
||||
"reverse_order_checkbox": "Reverse Order",
|
||||
"reverse_order_checkbox": "Reihenfolge umkehren",
|
||||
"chat_attachment": "Anlage{index}",
|
||||
"empty_message": "Freie Chatbotschaft",
|
||||
"message_parse_failed": "Versäumte Nachricht",
|
||||
@@ -245,14 +245,14 @@
|
||||
"choose_location_button": "Wählen Sie einen Standort",
|
||||
"manual_coordinates_hint": "Feinabstimmung der Koordinaten manuell.",
|
||||
"saved_coordinates_subtitle": "Verwalten Sie Ihre gespeicherten Spoof-Standorte",
|
||||
"teleport_to_friend_button": "Teleport to Friend",
|
||||
"teleport_to_friend_button": "Zu Freund teleportieren",
|
||||
"spoof_location_toggle": "Standort des Spoof",
|
||||
"suspend_location_updates": "Standort-Updates verhängen",
|
||||
"saved_coordinates_title": "Gespeicherte Koordinaten",
|
||||
"no_saved_coordinates_hint": "Keine gespeicherten Koordinaten",
|
||||
"delete_dialog_title": "Gespeicherte Koordinaten löschen",
|
||||
"delete_dialog_message": "Sind Sie sicher, dass Sie diese gespeicherte Koordinaten löschen möchten?",
|
||||
"teleport_to_friend_title": "Teleport to Friend",
|
||||
"delete_dialog_message": "Sind Sie sicher, dass Sie diese gespeicherte Koordinate l?schen m?chten?",
|
||||
"teleport_to_friend_title": "Zu Freund teleportieren",
|
||||
"search_bar": "Suche",
|
||||
"no_friends_map": "Keine Freunde auf der Karte",
|
||||
"no_friends_found": "Keine Freunde gefunden"
|
||||
@@ -277,7 +277,7 @@
|
||||
},
|
||||
"reset_config": {
|
||||
"title": "Zurück zur Übersicht",
|
||||
"content": "Sind Sie sicher, dass Sie die config zurücksetzen wollen?",
|
||||
"content": "Sind Sie sicher, dass Sie die Konfiguration zur?cksetzen m?chten?",
|
||||
"success_toast": "Zurücksetzen erfolgreich"
|
||||
},
|
||||
"quick_actions_dialog": {
|
||||
@@ -285,8 +285,8 @@
|
||||
"subtitle": "Zugriff auf Ihre Lieblingswerkzeuge schneller"
|
||||
},
|
||||
"export_config": {
|
||||
"title": "Export Sensitive Daten?",
|
||||
"content": "Möchten Sie die Config mit sensiblen Daten exportieren? (Sowie Ortskoordinaten, etc.)"
|
||||
"title": "Sensible Daten exportieren?",
|
||||
"content": "M?chten Sie die Konfiguration mit sensiblen Daten exportieren? (z. B. Standortkoordinaten usw.)"
|
||||
},
|
||||
"messaging_action": {
|
||||
"title": "Wählen Sie Inhaltstypen zu verarbeiten",
|
||||
@@ -347,9 +347,9 @@
|
||||
"no_repos_added": "Keine Repositorys hinzugefügt",
|
||||
"add_repo_button": "Repository hinzufügen",
|
||||
"add_repo_dialog_title": "Repository hinzufügen",
|
||||
"repo_url_label": "Repository URL",
|
||||
"repo_url_label": "Repository-URL",
|
||||
"add_button": "Hinzufügen",
|
||||
"invalid_repo_title": "Invalid Repository",
|
||||
"invalid_repo_title": "Ung?ltiges Repository",
|
||||
"invalid_repo_error": "Dieses Repository fehlt den benötigten Daten.",
|
||||
"repo_added_toast": "Repository hinzugefügt",
|
||||
"add_repo_failed_toast": "Fehler beim Hinzufügen von Repository:{message}",
|
||||
@@ -360,7 +360,7 @@
|
||||
},
|
||||
"friend_tracker": {
|
||||
"rules_tab": "Regeln",
|
||||
"logs_tab": "Logs",
|
||||
"logs_tab": "Protokolle",
|
||||
"catalog_button": "Katalog",
|
||||
"add_rule_button": "Artikel",
|
||||
"import_button": "Einfuhr",
|
||||
@@ -377,7 +377,7 @@
|
||||
"no_logs_found": "Keine Protokolle gefunden",
|
||||
"no_rules_found": "Keine Regeln gefunden",
|
||||
"export_logs_dialog_title": "Exportprotokolle",
|
||||
"export_logs_dialog_confirm_text": "Logs mit aktuellen Filtern exportieren?",
|
||||
"export_logs_dialog_confirm_text": "Protokolle mit aktuellen Filtern exportieren?",
|
||||
"export_as_button": "Ausfuhr{type}",
|
||||
"new_rule_title": "Neue Regel",
|
||||
"edit_rule_title": "Artikel",
|
||||
@@ -406,10 +406,10 @@
|
||||
"cannot_save_rule_dialog_text": "Füllen Sie die fehlenden Felder aus, um diese Regel zu speichern.",
|
||||
"duplicate_rule_name_dialog_title": "Duplikate Regelname",
|
||||
"duplicate_rule_name_dialog_text": "Eine Regel mit diesem Namen existiert bereits. Such dir einen neuen Namen.",
|
||||
"discard_changes_dialog_title": "Änderungen deaktivieren?",
|
||||
"discard_changes_dialog_text": "Sie haben keine Änderungen. Sie ablenken?",
|
||||
"discard_changes_dialog_title": "?nderungen verwerfen?",
|
||||
"discard_changes_dialog_text": "Sie haben ungespeicherte ?nderungen. Verwerfen?",
|
||||
"rule_subtitle": "Konfigurieren Sie Trigger und Scopes für diese Regel.",
|
||||
"discard_button": "Discard",
|
||||
"discard_button": "Verwerfen",
|
||||
"enabled_label": "Ermöglicht",
|
||||
"disabled_label": "Behinderte",
|
||||
"delete_rule_dialog_title": "Regel",
|
||||
@@ -423,13 +423,13 @@
|
||||
"export_dialog_title": "Ausfuhrregeln",
|
||||
"bulk_export_button": "Ausfuhr",
|
||||
"individual_export_button": "Einzelausfuhr",
|
||||
"reverse_order_checkbox": "Reverse Order",
|
||||
"reverse_order_checkbox": "Reihenfolge umkehren",
|
||||
"delete_logs_dialog_title": "Logs löschen",
|
||||
"delete_logs_dialog_confirm_text": "Löschen Sie alle Protokolle, die den aktuellen Filtern entsprechen?",
|
||||
"select_friends_groups_button": "Freunde / Gruppen auswählen"
|
||||
},
|
||||
"friend_tracker_export": {
|
||||
"title": "Export Friend Tracker",
|
||||
"title": "Freund-Tracker exportieren",
|
||||
"save_button": "Speichern",
|
||||
"back_button_description": "Zurück",
|
||||
"expand_button_description": "Kategorie erweitern oder zusammenbrechen",
|
||||
@@ -453,9 +453,9 @@
|
||||
"no_repos_added": "Keine Repositorys hinzugefügt",
|
||||
"add_repo_button": "Repository hinzufügen",
|
||||
"add_repo_dialog_title": "Repository hinzufügen",
|
||||
"repo_url_label": "Repository URL",
|
||||
"repo_url_label": "Repository-URL",
|
||||
"add_button": "Hinzufügen",
|
||||
"invalid_repo_title": "Invalid Repository",
|
||||
"invalid_repo_title": "Ung?ltiges Repository",
|
||||
"invalid_repo_error": "Dieses Repository fehlt den benötigten Daten.",
|
||||
"repo_added_toast": "Repository hinzugefügt",
|
||||
"add_repo_failed_toast": "Fehler beim Hinzufügen von Repository:{message}",
|
||||
@@ -532,11 +532,11 @@
|
||||
}
|
||||
},
|
||||
"auto_open_snaps": {
|
||||
"name": "Auto Open Snaps",
|
||||
"name": "Snaps automatisch ?ffnen",
|
||||
"description": "Öffnet automatisch Snaps beim Empfang",
|
||||
"options": {
|
||||
"blacklist": "Ausschließen von Auto Open Snaps",
|
||||
"whitelist": "Auto Open Snaps"
|
||||
"whitelist": "Snaps automatisch ?ffnen"
|
||||
}
|
||||
},
|
||||
"hide_friend_feed": {
|
||||
@@ -546,7 +546,7 @@
|
||||
"name": "E2E Verschlüsselung verwenden"
|
||||
},
|
||||
"pin_conversation": {
|
||||
"name": "Pin Conversation"
|
||||
"name": "Unterhaltung anheften"
|
||||
},
|
||||
"exclude_message_logger": {
|
||||
"name": "Ausschließen von Message Logger"
|
||||
@@ -595,7 +595,7 @@
|
||||
},
|
||||
"actions": {
|
||||
"clean_snapchat_cache": {
|
||||
"name": "Clean Snapchat Cache",
|
||||
"name": "Snapchat-Cache leeren",
|
||||
"description": "Reinigt den Snapchat Cache"
|
||||
},
|
||||
"manage_friend_list": {
|
||||
@@ -607,15 +607,15 @@
|
||||
"description": "Exportiert Konversationsnachrichten in eine JSON/HTML/TXT-Datei"
|
||||
},
|
||||
"export_memories": {
|
||||
"name": "Export Memories",
|
||||
"name": "Memories exportieren",
|
||||
"description": "Exportiert Speicher in eine ZIP-Datei"
|
||||
},
|
||||
"bulk_messaging_action": {
|
||||
"name": "Bulk Messaging Action",
|
||||
"name": "Massen-Nachrichtenaktion",
|
||||
"description": "Führt Operationen wie das Löschen von Freunden oder das Löschen von Gesprächen durch"
|
||||
},
|
||||
"regen_mappings": {
|
||||
"name": "Regenerate Mappings",
|
||||
"name": "Zuordnungen neu erstellen",
|
||||
"description": "Manuelle Mappings regenerieren"
|
||||
},
|
||||
"change_language": {
|
||||
@@ -668,7 +668,7 @@
|
||||
"auto_delete_sent_messages": "🗑️ Auto Löschen Sent-Nachrichten",
|
||||
"mark_snaps_as_seen": "👀 Mark Snaps als gesehen",
|
||||
"mark_stories_as_seen_locally": "👀 Mark Stories als lokal gesehen",
|
||||
"conversation_info": "👤 Conversation Info",
|
||||
"conversation_info": "?? Gespr?chsinfo",
|
||||
"e2e_encryption": "🔒 E2E Verschlüsselung verwenden",
|
||||
"message_logger": "📝 Nachrichtenlogger",
|
||||
"auto_read": "✅ Auto Lesen",
|
||||
@@ -726,9 +726,9 @@
|
||||
"speaking": "Apropos",
|
||||
"chat_reaction": "DM-Reaktion",
|
||||
"group_chat_reaction": "Gruppenreaktion",
|
||||
"initiate_audio": "Incoming Audio Call",
|
||||
"initiate_audio": "Eingehender Audioanruf",
|
||||
"abandon_audio": "Versäumter Audio-Anruf",
|
||||
"initiate_video": "Incoming Video Call",
|
||||
"initiate_video": "Eingehender Videoanruf",
|
||||
"abandon_video": "Vermisste Videoanruf",
|
||||
"map_live_location": "Karte Live Standort"
|
||||
},
|
||||
@@ -786,13 +786,13 @@
|
||||
"null": "Spuck nicht Quelle"
|
||||
},
|
||||
"custom_streaks_expiration_format": {
|
||||
"null": "System Default"
|
||||
"null": "Systemstandard"
|
||||
},
|
||||
"preferred_transcription_lang": {
|
||||
"null": "Snapchat Default verwenden"
|
||||
},
|
||||
"custom_emoji_font": {
|
||||
"null": "Default Emoji Font"
|
||||
"null": "Standard-Emoji-Schriftart"
|
||||
},
|
||||
"custom_shared_library": {
|
||||
"null": "Standardbibliothek verwenden"
|
||||
@@ -833,18 +833,18 @@
|
||||
"null": "Standard-Code"
|
||||
},
|
||||
"preset": {
|
||||
"null": "Default Preset"
|
||||
"null": "Standard-Preset"
|
||||
},
|
||||
"app_appearance_override": {
|
||||
"title": "Erscheinung"
|
||||
},
|
||||
"gallery_media_send_override": {
|
||||
"always_ask": "Immer fragen",
|
||||
"ORIGINAL": "Original Media",
|
||||
"ORIGINAL": "Originalmedien",
|
||||
"NOTE": "Audionote",
|
||||
"SNAP": "Schnapper",
|
||||
"SAVEABLE_SNAP": "Speichern von Snap",
|
||||
"null": "Snapchat Default"
|
||||
"null": "Snapchat-Standard"
|
||||
},
|
||||
"strip_media_metadata": {
|
||||
"hide_caption_text": "Verstecken Caption Text",
|
||||
@@ -875,7 +875,7 @@
|
||||
"camera": "Kamera",
|
||||
"discover": "Entdecken",
|
||||
"spotlight": "Scheinwerfer",
|
||||
"null": "Snapchat Default"
|
||||
"null": "Snapchat-Standard"
|
||||
},
|
||||
"spotlight_comments_username_icon": {
|
||||
"user": "Benutzername Icon",
|
||||
@@ -888,7 +888,7 @@
|
||||
"null": "Automatische Übersetzung"
|
||||
},
|
||||
"update_check_frequency": {
|
||||
"null": "Auto"
|
||||
"null": "Automatisch"
|
||||
},
|
||||
"snapchat_plus": {
|
||||
"not_subscribed": "Nicht angemeldet",
|
||||
@@ -902,9 +902,9 @@
|
||||
"null": "Fehler"
|
||||
},
|
||||
"old_bitmoji_selfie": {
|
||||
"2d": "2D Bitmoji",
|
||||
"3d": "3D Bitmoji",
|
||||
"null": "Default Bitmoji"
|
||||
"2d": "2D-Bitmoji",
|
||||
"3d": "3D-Bitmoji",
|
||||
"null": "Standard-Bitmoji"
|
||||
},
|
||||
"disable_confirmation_dialogs": {
|
||||
"erase_message": "Nachricht löschen",
|
||||
@@ -1017,18 +1017,18 @@
|
||||
},
|
||||
"ai_response_style": {
|
||||
"casual": "Lässig",
|
||||
"formal": "Formal",
|
||||
"formal": "Formell",
|
||||
"friendly": "Freundlichkeit",
|
||||
"humorous": "Humorvoll",
|
||||
"empathetic": "Empfängnis",
|
||||
"toxic": "Edgy",
|
||||
"toxic": "Provokant",
|
||||
"busy": "Beschäftigt"
|
||||
},
|
||||
"ai_temperature": {
|
||||
"0.7": "Bilanziert (0.7)"
|
||||
},
|
||||
"ai_response_language": {
|
||||
"auto": "Auto",
|
||||
"auto": "Automatisch",
|
||||
"en": "Englisch",
|
||||
"es": "Spanisch",
|
||||
"fr": "Französisch",
|
||||
@@ -1090,7 +1090,7 @@
|
||||
"external_media_messages": "Externe Medien",
|
||||
"voice_note_messages": "Sprachhinweise",
|
||||
"sticker_messages": "Aufkleber",
|
||||
"tiny_snap_messages": "Tiny Snaps",
|
||||
"tiny_snap_messages": "Mini-Snaps",
|
||||
"map_reaction_messages": "Kartenreaktionen",
|
||||
"half_swipes": "Halbschweine"
|
||||
},
|
||||
@@ -1151,7 +1151,7 @@
|
||||
"description": "Verhindert, dass Ihr Standort aktualisiert wird"
|
||||
},
|
||||
"spoof_battery_level": {
|
||||
"name": "Spoof Battery Level",
|
||||
"name": "Akkustand vort?uschen",
|
||||
"description": "Läust den Batteriepegel Ihres Geräts auf der Karte\nWert muss zwischen 0 und 100 liegen"
|
||||
},
|
||||
"spoof_headphones": {
|
||||
@@ -1255,7 +1255,7 @@
|
||||
"description": "Stellt die Standardgeschwindigkeit für die Wiedergabe von Videos fest\nWert muss zwischen 0,1 und 4,0 liegen"
|
||||
},
|
||||
"video_playback_rate_slider": {
|
||||
"name": "Video Playback Rate Slider",
|
||||
"name": "Regler f?r Video-Wiedergabegeschwindigkeit",
|
||||
"description": "Fügt einen Slider im Kontextmenü der Oper hinzu, um die Videowiedergaberate zu ändern\nHinweis: Änderungen gelten nur für nachfolgende Videos"
|
||||
},
|
||||
"disable_google_play_dialogs": {
|
||||
@@ -1263,7 +1263,7 @@
|
||||
"description": "Verhindern Sie Google Play Services-Verfügbarkeitsdialoge von angezeigt werden"
|
||||
},
|
||||
"default_volume_controls": {
|
||||
"name": "Default Volume Controls",
|
||||
"name": "Standard-Lautst?rkeregelung",
|
||||
"description": "Erzwingt Snapchat, Systemvolumensteuerungen zu verwenden"
|
||||
},
|
||||
"disable_telecom_framework": {
|
||||
@@ -1305,7 +1305,7 @@
|
||||
"description": "Ermöglicht die gleichen Medien mehrfach heruntergeladen werden"
|
||||
},
|
||||
"merge_overlays": {
|
||||
"name": "Merge Overlays",
|
||||
"name": "Overlays zusammenf?hren",
|
||||
"description": "Kombiniert den Text und die Medien eines Snap in eine einzelne Datei"
|
||||
},
|
||||
"force_image_format": {
|
||||
@@ -1349,11 +1349,11 @@
|
||||
"description": "Setzen Sie den konstanten Ratefaktor für den Video-Encoder\nVon 0 bis 51 für libx264"
|
||||
},
|
||||
"video_bitrate": {
|
||||
"name": "Video Bitrate",
|
||||
"name": "Video-Bitrate",
|
||||
"description": "Setzen Sie die Videobitrate (kbps)"
|
||||
},
|
||||
"audio_bitrate": {
|
||||
"name": "Audio Bitrate",
|
||||
"name": "Audio-Bitrate",
|
||||
"description": "Einstellen der Audiobitrate (kbps)"
|
||||
},
|
||||
"custom_video_codec": {
|
||||
@@ -1399,7 +1399,7 @@
|
||||
"description": "Zeigt eine kleine Vorschau neben ungesehenen Snaps im Chat an"
|
||||
},
|
||||
"bootstrap_override": {
|
||||
"name": "Bootstrap Override",
|
||||
"name": "Bootstrap-Override",
|
||||
"description": "Überträgt Benutzeroberfläche Bootstrap Einstellungen",
|
||||
"properties": {
|
||||
"app_appearance": {
|
||||
@@ -1407,13 +1407,13 @@
|
||||
"description": "Setzt eine anhaltende Erscheinung"
|
||||
},
|
||||
"home_tab": {
|
||||
"name": "Home Tab",
|
||||
"name": "Startseite-Tab",
|
||||
"description": "Übertreibt die Startkarte beim Öffnen von Snapchat"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_friend_nametags": {
|
||||
"name": "Enhanced Friend Map Nametags",
|
||||
"name": "Erweiterte Freundkarten-Namensschilder",
|
||||
"description": "Verbessert die Namensschilder von Freunden auf der Snapmap"
|
||||
},
|
||||
"prevent_message_list_auto_scroll": {
|
||||
@@ -1445,7 +1445,7 @@
|
||||
"description": "Wählen Sie aus, welche UI-Komponenten zu verstecken sind"
|
||||
},
|
||||
"opera_media_quick_info": {
|
||||
"name": "Opera Media Quick Info",
|
||||
"name": "Opera Media Schnellinfo",
|
||||
"description": "Zeigt nützliche Informationen von Medien wie Erstellungsdatum im Kontextmenü Opera Viewer"
|
||||
},
|
||||
"old_bitmoji_selfie": {
|
||||
@@ -1465,7 +1465,7 @@
|
||||
"description": "Schließen Sie das Friend Feed Menü automatisch nach dem Drücken einer Einstelltaste"
|
||||
},
|
||||
"vertical_story_viewer": {
|
||||
"name": "Vertical Story Viewer",
|
||||
"name": "Vertikaler Story-Viewer",
|
||||
"description": "Ermöglicht den vertikalen Story Viewer für alle Geschichten"
|
||||
},
|
||||
"enable_friend_feed_menu_bar": {
|
||||
@@ -1515,7 +1515,7 @@
|
||||
"description": "Verhindert, dass jemand weiß, dass Sie ihre Geschichte neu beobachtet haben"
|
||||
},
|
||||
"hide_peek_a_peek": {
|
||||
"name": "Hide Peek-a-Peek",
|
||||
"name": "Peek-a-Peek ausblenden",
|
||||
"description": "Verhindert die Benachrichtigung von gesendet werden, wenn Sie halb schwipe in einen Chat"
|
||||
},
|
||||
"hide_bitmoji_presence": {
|
||||
@@ -1539,11 +1539,11 @@
|
||||
"description": "Fügt eine Schaltfläche hinzu, um einen Snap zu markieren, wie beim Betrachten gesehen.\nDies funktioniert auch, wenn Stealth-Modus aktiviert ist"
|
||||
},
|
||||
"skip_when_marking_as_seen": {
|
||||
"name": "Skip When Marking as Seen",
|
||||
"name": "Beim Als-gesehen-Markieren ?berspringen",
|
||||
"description": "Überspringen Sie automatisch zum nächsten Snap, wenn Sie einen Snap wie gesehen markieren.\nVerwendung in Kombination mit Mark Snap als Seen Button"
|
||||
},
|
||||
"loop_media_playback": {
|
||||
"name": "Loop Media Playback",
|
||||
"name": "Medienwiedergabe in Schleife",
|
||||
"description": "Loops Medienwiedergabe beim Anzeigen von Snaps / Stories"
|
||||
},
|
||||
"disable_replay_in_ff": {
|
||||
@@ -1707,7 +1707,7 @@
|
||||
"description": "Wählen Sie, wie Galeriemedien gesendet werden"
|
||||
},
|
||||
"include_camera_snaps": {
|
||||
"name": "Include Camera Snaps",
|
||||
"name": "Kamera-Snaps einbeziehen",
|
||||
"description": "Zeigen Sie auch den Override Dialog für Kamera Snaps"
|
||||
}
|
||||
}
|
||||
@@ -1729,7 +1729,7 @@
|
||||
"description": "Erlauben Sie, Gruppeninformationen nach dem Kick zu sehen"
|
||||
},
|
||||
"double_tap_chat_action": {
|
||||
"name": "Double Tap Chat Action",
|
||||
"name": "Doppeltippen-Chat-Aktion",
|
||||
"description": "Führen Sie eine benutzerdefinierte Aktion, wenn Sie eine Nachricht im Chat doppelt tippen"
|
||||
},
|
||||
"double_tap_chat_action_custom_emoji": {
|
||||
@@ -1761,7 +1761,7 @@
|
||||
"description": "Verwenden Sie KI, um intelligente Auto-Replies anstelle von Template-Nachrichten zu generieren"
|
||||
},
|
||||
"ai_provider": {
|
||||
"name": "AI Provider",
|
||||
"name": "KI-Anbieter",
|
||||
"description": "Wählen Sie aus, welchen KI-Dienst zur Generierung von Antworten verwendet wird"
|
||||
},
|
||||
"ai_endpoint_url": {
|
||||
@@ -1777,11 +1777,11 @@
|
||||
"description": "API-Schlüssel zur Authentifizierung mit dem AI-Service"
|
||||
},
|
||||
"ai_system_prompt": {
|
||||
"name": "AI System Prompt",
|
||||
"name": "KI-System-Prompt",
|
||||
"description": "Systemaufforderung, die die Persönlichkeit und das Verhalten der KI definiert"
|
||||
},
|
||||
"ai_max_tokens": {
|
||||
"name": "AI Max Tokens",
|
||||
"name": "KI-Max.-Tokens",
|
||||
"description": "Maximale Anzahl von Token (Worte) kann die KI in Antworten verwenden"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1817,11 +1817,11 @@
|
||||
"description": "Verwenden Sie Template-Nachrichten, wenn KI eine Antwort nicht generiert"
|
||||
},
|
||||
"ai_request_timeout": {
|
||||
"name": "AI Request Timeout",
|
||||
"name": "KI-Anfrage-Timeout",
|
||||
"description": "Maximale Zeit, um auf KI-Antwort zu warten (in Sekunden)"
|
||||
},
|
||||
"ai_retry_attempts": {
|
||||
"name": "AI Retry Attempts",
|
||||
"name": "KI-Wiederholungsversuche",
|
||||
"description": "Anzahl der Zeiten, um KI-Anforderungen wiederherzustellen, wenn sie scheitern"
|
||||
}
|
||||
}
|
||||
@@ -1895,11 +1895,11 @@
|
||||
"description": "Ermöglicht Auto Open Snaps im Hintergrund laufen. Anmerkung: Dies wird Ihren Akku deutlich entleeren"
|
||||
},
|
||||
"min_delay": {
|
||||
"name": "Min Delay (ms)",
|
||||
"name": "Min. Verz?gerung (ms)",
|
||||
"description": "Mindestverzögerung in Millisekunden vor dem Öffnen eines Schnapps"
|
||||
},
|
||||
"max_delay_ms": {
|
||||
"name": "Max Delay (ms)",
|
||||
"name": "Max. Verz?gerung (ms)",
|
||||
"description": "Maximale Verzögerung in Millisekunden vor dem Öffnen eines Schnapps"
|
||||
},
|
||||
"queue_size": {
|
||||
@@ -1907,11 +1907,11 @@
|
||||
"description": "Maximale Anzahl der Snaps in Warteschlange zu halten"
|
||||
},
|
||||
"retry_attempts": {
|
||||
"name": "Retry Attempts",
|
||||
"name": "Wiederholungsversuche",
|
||||
"description": "Anzahl der Zeiten, um einen Snap zu öffnen, wenn es versagt"
|
||||
},
|
||||
"retry_delay": {
|
||||
"name": "Retry Delay (ms)",
|
||||
"name": "Wiederholungsverz?gerung (ms)",
|
||||
"description": "Verzögerung in Millisekunden zwischen Retry-Ansätzen"
|
||||
}
|
||||
}
|
||||
@@ -2068,7 +2068,7 @@
|
||||
"description": "Setzt eine benutzerdefinierte Kameraauflösung, Breite x Höhe (z.B. 1920x1080).\nDie benutzerdefinierte Auflösung muss von Ihrem Gerät unterstützt werden"
|
||||
},
|
||||
"front_custom_frame_rate": {
|
||||
"name": "Front Custom Frame Rate",
|
||||
"name": "Benutzerdefinierte Front-Bildrate",
|
||||
"description": "Übertreibt die vordere Kamera-Rahmenrate"
|
||||
},
|
||||
"back_custom_frame_rate": {
|
||||
@@ -2076,7 +2076,7 @@
|
||||
"description": "Übertreibt die Rückfahrkamera-Rahmenrate"
|
||||
},
|
||||
"force_camera_source_encoding": {
|
||||
"name": "Force Camera Source Encoding",
|
||||
"name": "Kameraquellenkodierung erzwingen",
|
||||
"description": "Erzwingt die Kameraquellencodierung"
|
||||
},
|
||||
"startup_default_camera": {
|
||||
@@ -2090,7 +2090,7 @@
|
||||
}
|
||||
},
|
||||
"streaks_reminder": {
|
||||
"name": "Streaks Reminder",
|
||||
"name": "Streaks-Erinnerung",
|
||||
"description": "Benachrichtigt Sie regelmäßig über Ihre Streaks",
|
||||
"properties": {
|
||||
"interval": {
|
||||
@@ -2132,11 +2132,11 @@
|
||||
"description": "Gibt mehr Optionen für den selbstzerstörenden Timer beim Senden eines Snap"
|
||||
},
|
||||
"composer_console": {
|
||||
"name": "Composer Console",
|
||||
"name": "Composer-Konsole",
|
||||
"description": "Ermöglicht die Ausführung von JavaScript-Code in Composer (nur arm64)"
|
||||
},
|
||||
"composer_logs": {
|
||||
"name": "Composer Logs",
|
||||
"name": "Composer-Logs",
|
||||
"description": "Redirects Konsolenprotokolle von Composer zu PurrfectSnap"
|
||||
}
|
||||
}
|
||||
@@ -2176,11 +2176,11 @@
|
||||
"description": "Kraftverkehr zum Bericht WLAN statt mobiler Daten"
|
||||
},
|
||||
"spoof_device_id": {
|
||||
"name": "Spoof Device ID",
|
||||
"name": "Ger?te-ID spoofen",
|
||||
"description": "Überschreiben Sie die Android-ID an Snapchat gesendet",
|
||||
"properties": {
|
||||
"spoof_android_id": {
|
||||
"name": "Spoof Android ID",
|
||||
"name": "Android-ID spoofen",
|
||||
"description": "Überschreiben Sie die Android-ID an Snapchat mit einem benutzerdefinierten Wert"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -2204,7 +2204,7 @@
|
||||
"description": "Konvertiert Snaps, um externe Medien lokal zu chatten. Dies erscheint im Kontextmenü des Chat-Downloads"
|
||||
},
|
||||
"media_file_picker": {
|
||||
"name": "Media File Picker",
|
||||
"name": "Medien-Dateiauswahl",
|
||||
"description": "Erlaubt Ihnen, jede Video/Audio-Datei aus der Galerie auszuwählen"
|
||||
},
|
||||
"story_logger": {
|
||||
@@ -2244,7 +2244,7 @@
|
||||
}
|
||||
},
|
||||
"voice_note_auto_play": {
|
||||
"name": "Voice Note Auto Play",
|
||||
"name": "Sprachnotizen automatisch abspielen",
|
||||
"description": "Spielt automatisch die nächste Sprachnote, nachdem die aktuelle beendet ist"
|
||||
},
|
||||
"friend_notes": {
|
||||
@@ -2260,7 +2260,7 @@
|
||||
"description": "Versuchen Sie, das Friend Feed Menü zu reparieren, als wenn das Gerät offline ist, kann es nicht korrekt angezeigt werden"
|
||||
},
|
||||
"app_lock": {
|
||||
"name": "App Lock",
|
||||
"name": "App-Sperre",
|
||||
"description": "Verhindert Zugriff auf Snapchat ohne Passcode",
|
||||
"properties": {
|
||||
"lock_on_resume": {
|
||||
@@ -2270,7 +2270,7 @@
|
||||
}
|
||||
},
|
||||
"infinite_story_boost": {
|
||||
"name": "Infinite Story Boost",
|
||||
"name": "Unendlicher Story-Boost",
|
||||
"description": "Bypass the Story Boost Limit Verzögerung"
|
||||
},
|
||||
"meo_passcode_bypass": {
|
||||
@@ -2278,7 +2278,7 @@
|
||||
"description": "Bypass the My Eyes Nur Passcode\nDies funktioniert nur, wenn der Passcode korrekt eingegeben wurde, bevor"
|
||||
},
|
||||
"no_friend_score_delay": {
|
||||
"name": "No Friend Score Delay",
|
||||
"name": "Keine Freundes-Score-Verz?gerung",
|
||||
"description": "Entfernt die Verzögerung beim Betrachten eines Friends Score"
|
||||
},
|
||||
"best_friend_pinning": {
|
||||
@@ -2301,10 +2301,10 @@
|
||||
},
|
||||
"add_friend_source_spoof": {
|
||||
"name": "Friend Source Spoof",
|
||||
"description": "Spoofs the source of a Friend Request"
|
||||
"description": "Spooft die Quelle einer Freundschaftsanfrage"
|
||||
},
|
||||
"hidden_snapchat_plus_features": {
|
||||
"name": "Hidden Snapchat Plus Features",
|
||||
"name": "Versteckte Snapchat-Plus-Funktionen",
|
||||
"description": "Ermöglicht unveröffentlichte / Beta Snapchat Plus Funktionen\nVielleicht nicht auf älteren Snapchat-Versionen arbeiten"
|
||||
},
|
||||
"custom_streaks_expiration_format": {
|
||||
@@ -2451,11 +2451,11 @@
|
||||
},
|
||||
"gallery_media_send_override": {
|
||||
"always_ask": "Immer fragen",
|
||||
"ORIGINAL": "Original Media",
|
||||
"ORIGINAL": "Originalmedien",
|
||||
"NOTE": "Audionote",
|
||||
"SNAP": "Schnapper",
|
||||
"SAVEABLE_SNAP": "Speichern von Snap",
|
||||
"null": "Snapchat Default",
|
||||
"null": "Snapchat-Standard",
|
||||
"multiple_media_toast": "Sie können nur eine Medien zu einer Zeit senden"
|
||||
},
|
||||
"mark_as_seen": {
|
||||
@@ -2500,7 +2500,7 @@
|
||||
"following": "Folgen",
|
||||
"suggested": "Vorschlag",
|
||||
"incoming": "Einkommen",
|
||||
"incoming_follower": "Incoming Follower"
|
||||
"incoming_follower": "Eingehender Follower"
|
||||
},
|
||||
"bulk_messaging_action": {
|
||||
"actions.title": "Maßnahmen",
|
||||
@@ -2508,14 +2508,14 @@
|
||||
"progress_status": "Verarbeitung{index}von{total}",
|
||||
"selection_dialog_continue_button": "Fortsetzung",
|
||||
"confirmation_dialog": {
|
||||
"title": "Bist du sicher?",
|
||||
"title": "Sind Sie sicher?",
|
||||
"message": "Dies wird alle ausgewählten Auswirkungen haben. Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
},
|
||||
"actions": {
|
||||
"remove_friends": "Freunde entfernen",
|
||||
"clear_conversations": "Klare Konversationen",
|
||||
"clear_friend_feed": "Klarer Freund Feed ({count})",
|
||||
"unfollow": "Unfollow",
|
||||
"unfollow": "Nicht mehr folgen",
|
||||
"remove": "Entfernen"
|
||||
},
|
||||
"leave_groups": "Weg{count}gruppen",
|
||||
@@ -2599,12 +2599,12 @@
|
||||
"open": "Öffnen",
|
||||
"download": "Downloads",
|
||||
"send": "Bitte",
|
||||
"restore_original": "Restore Original",
|
||||
"restore_original": "Original wiederherstellen",
|
||||
"convert_external_media": "Externe Medien umrechnen"
|
||||
},
|
||||
"tracker_events": {
|
||||
"conversation_enter": "Konversation eingeben",
|
||||
"conversation_exit": "Conversation Exit",
|
||||
"conversation_exit": "Unterhaltung verlassen",
|
||||
"started_typing": "Gestartet Typing",
|
||||
"stopped_typing": "Gestoppte Typisierung",
|
||||
"started_speaking": "Gestartet Apropos",
|
||||
@@ -2619,10 +2619,10 @@
|
||||
"message_reaction_add": "Nachrichtenreaktion Hinzufügen",
|
||||
"message_reaction_remove": "Nachricht Reaction Entfernen",
|
||||
"snap_opened": "Snap geöffnet",
|
||||
"snap_replayed": "Snap Replayed",
|
||||
"snap_replayed_twice": "Snap Replayed Twice",
|
||||
"snap_screenshot": "Snap Screenshot",
|
||||
"snap_screen_record": "Snap Screen Record",
|
||||
"snap_replayed": "Snap erneut abgespielt",
|
||||
"snap_replayed_twice": "Snap zweimal erneut abgespielt",
|
||||
"snap_screenshot": "Snap-Screenshot",
|
||||
"snap_screen_record": "Snap-Bildschirmaufnahme",
|
||||
"i_can_see_you": "Ich kann dich sehen"
|
||||
},
|
||||
"cleared_from_feed": "Vom Futter befreit",
|
||||
@@ -2646,8 +2646,8 @@
|
||||
"background_option": "Hintergrund"
|
||||
},
|
||||
"call_start_confirmation": {
|
||||
"dialog_title": "Start Call",
|
||||
"dialog_message": "Willst du sicher einen Anruf starten?"
|
||||
"dialog_title": "Anruf starten",
|
||||
"dialog_message": "Sind Sie sicher, dass Sie einen Anruf starten m?chten?"
|
||||
},
|
||||
"half_swipe_notifier": {
|
||||
"notification_channel_name": "Halbschwein",
|
||||
@@ -2661,7 +2661,7 @@
|
||||
"gif": "GIF",
|
||||
"external_media": "Externe Medien",
|
||||
"note": "Anmerkung",
|
||||
"original_story": "Original Story"
|
||||
"original_story": "Original-Story"
|
||||
},
|
||||
"select_attachments_title": "Anhänge auswählen",
|
||||
"download_started_toast": "Download gestartet",
|
||||
@@ -2701,8 +2701,8 @@
|
||||
},
|
||||
"confirmation_dialogs": {
|
||||
"title": "End-to-End-Verschlüsselung",
|
||||
"confirmation_1": "WARNING: Dies wird Ihren vorhandenen Schlüssel überschreiben. Sie werden den Zugriff auf alle verschlüsselten Nachrichten von diesem Freund verlieren. Bist du sicher, dass du weitermachen willst?",
|
||||
"confirmation_2": "Bist du sicher, dass du weitermachen willst? Das ist deine letzte Chance."
|
||||
"confirmation_1": "WARNUNG: Dadurch wird Ihr vorhandener Schl?ssel ?berschrieben. Sie verlieren den Zugriff auf alle verschl?sselten Nachrichten von diesem Freund. Sind Sie sicher, dass Sie fortfahren m?chten?",
|
||||
"confirmation_2": "Sind Sie wirklich sicher, dass Sie fortfahren m?chten? Dies ist Ihre letzte Chance, abzubrechen."
|
||||
},
|
||||
"unencrypted_conversation_send_failure_toast": "Sie können verschlüsselte Inhalte nicht an verschlüsselte und unverschlüsselte Gespräche senden!",
|
||||
"native_hooks_send_failure_toast": "Nicht zu senden! Bitte aktivieren Sie Native Hooks in den Einstellungen.",
|
||||
@@ -2720,13 +2720,13 @@
|
||||
"incoming_secret_message": "Dein Freund hat deinen öffentlichen Schlüssel akzeptiert. Klicken Sie unten, um das Geheimnis zu akzeptieren."
|
||||
},
|
||||
"auto_open_snaps": {
|
||||
"title": "Auto Open Snaps",
|
||||
"priority_title": "Auto Open Snaps (Priority)",
|
||||
"title": "Snaps automatisch ?ffnen",
|
||||
"priority_title": "Snaps automatisch ?ffnen (Priorit?t)",
|
||||
"error_title": "Auto Open Snaps (Fehler)",
|
||||
"channel_description": "Notifications for auto-opening Snaps queue status",
|
||||
"priority_channel_description": "Hohe Prioritätsmeldungen für Auto-Öffnungs-Snaps",
|
||||
"error_channel_description": "Fehlerbenachrichtigungen, wenn Auto-Öffnung Snaps ausfällt",
|
||||
"paused_status": "Auto Open Snaps paused",
|
||||
"paused_status": "Snaps automatisch ?ffnen pausiert",
|
||||
"processing_status": "Verarbeitung von Schnappern:{queued}in warteschlange,{processed}verarbeitet",
|
||||
"monitor_status": "Überwachung...",
|
||||
"recent_snaps": "Neueste Snaps",
|
||||
@@ -2735,7 +2735,7 @@
|
||||
"action_clear": "Löschen Sie Ihre Suche",
|
||||
"action_reset": "Zurück zur Übersicht",
|
||||
"error_content": "Nicht zu öffnen Snap von{sender}:{error}",
|
||||
"resumed_feedback": "Auto Open Resumed",
|
||||
"resumed_feedback": "Automatisches ?ffnen fortgesetzt",
|
||||
"paused_feedback": "Auto öffnen Pausen",
|
||||
"resumed_message": "Die Verarbeitung wird automatisch für Queued Snaps fortgesetzt",
|
||||
"paused_message": "Verarbeitung pausiert. Warteschlangen ({count}schnapper)",
|
||||
@@ -2759,16 +2759,16 @@
|
||||
"notification_statistics": "STATISTIK",
|
||||
"notification_queue_size": "Suchgröße",
|
||||
"notification_total_opened": "Gesamt Snaps geöffnet",
|
||||
"notification_queue_preview": "QUEUE PREVIEW",
|
||||
"notification_queue_preview": "WARTESCHLANGE-VORSCHAU",
|
||||
"notification_processing_continue": "Die Verarbeitung wird automatisch fortgesetzt...",
|
||||
"notification_no_snaps_queue": "Keine Momente.",
|
||||
"notification_queue_cleared_opened": "Gelöschte Queue ({opened}geöffnet)",
|
||||
"content_type_photo_video_snap": "Foto/Video Snap",
|
||||
"conversation_type_group_with_name": "Gruppe:{name}",
|
||||
"delete_logs_title": "Logs löschen?",
|
||||
"delete_logs_title": "Protokolle l?schen?",
|
||||
"delete_logs_progress": "Löschung{count}logs...",
|
||||
"delete_logs_description": "Dies wird Protokolle basierend auf dem aktuellen Filter und der Suchanfrage löschen. Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"export_logs_title": "Logs exportieren?",
|
||||
"export_logs_title": "Protokolle exportieren?",
|
||||
"export_logs_progress": "Logs exportieren...",
|
||||
"export_logs_description": "Dies exportiert Protokolle basierend auf dem aktuellen Filter und der Suchanfrage.",
|
||||
"export_logs_as": "Ausfuhr{type}",
|
||||
@@ -2830,7 +2830,7 @@
|
||||
"i_can_see_you": "{friend}tätigkeit{conversation}:{details}"
|
||||
},
|
||||
"friend_mutation_observer": {
|
||||
"notification_channel_name": "Friend Mutation Observer",
|
||||
"notification_channel_name": "Freund-?nderungsbeobachter",
|
||||
"friend_removed": "{username}hat dich als freund entfernt",
|
||||
"birthday_removed": "{username}hat ihren geburtstag entfernt ({birthday})",
|
||||
"birthday_added": "{username}hat ihren geburtstag hinzugefügt ({birthday})",
|
||||
@@ -2921,7 +2921,7 @@
|
||||
},
|
||||
"tracker": {
|
||||
"tabs": {
|
||||
"logs": "Logs",
|
||||
"logs": "Protokolle",
|
||||
"rules": "Regeln"
|
||||
},
|
||||
"actions": {
|
||||
@@ -2996,7 +2996,7 @@
|
||||
"i_can_see_you_entered": "Eingetragen",
|
||||
"i_can_see_you_left": "Links",
|
||||
"i_can_see_you_duration": "Dauer",
|
||||
"i_can_see_you_not_available": "N/A",
|
||||
"i_can_see_you_not_available": "k. A.",
|
||||
"i_can_see_you_unit_hour": "h",
|
||||
"i_can_see_you_unit_minute": "m",
|
||||
"i_can_see_you_unit_second": "s",
|
||||
@@ -3078,7 +3078,7 @@
|
||||
"theme_mode_system": "System",
|
||||
"theme_mode_light": "Licht",
|
||||
"theme_mode_dark": "Dunkel",
|
||||
"test_mode_label": "Enable PurrAura",
|
||||
"test_mode_label": "PurrAura aktivieren",
|
||||
"disable_feature_loading_label": "Deaktivieren der Funktion",
|
||||
"disable_auto_mapper_label": "Auto Mapper deaktivieren",
|
||||
"disable_bypass_indicator_label": "Bypass Indikator deaktivieren",
|
||||
@@ -3095,7 +3095,7 @@
|
||||
"date_range": "Datumsbereich",
|
||||
"select": "Wählen",
|
||||
"sort_by_folder": "Sortieren nach Ordner",
|
||||
"include_my_eyes_only": "Include My Eyes Only",
|
||||
"include_my_eyes_only": "My Eyes Only einschlie?en",
|
||||
"cancel": "Abbrechen",
|
||||
"export": "Ausfuhr",
|
||||
"quit": "Quitten",
|
||||
@@ -3158,7 +3158,7 @@
|
||||
"failed_to_get_file": "Nicht verfügbar",
|
||||
"download_button": "Downloads",
|
||||
"chat_attachment": "Anlage{index}",
|
||||
"log_header_format": "{username}?{type}?{date}",
|
||||
"log_header_format": "{username} ? {type} ? {date}",
|
||||
"edited_at_text": "Bearbeitet auf \"{message}\"{date}",
|
||||
"list_group_format": "Gruppe{name}",
|
||||
"list_friend_format": "Freund{name}",
|
||||
@@ -3170,7 +3170,7 @@
|
||||
},
|
||||
"debug_dialogs": {
|
||||
"info": "Info",
|
||||
"refs": "Refs",
|
||||
"refs": "Referenzen",
|
||||
"arroyo": "Arroyo",
|
||||
"message": "Nachricht",
|
||||
"media_references": "Medienberichte",
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Valitse kieli",
|
||||
@@ -84,7 +84,7 @@
|
||||
"clear_button": "Tyhjennä",
|
||||
"view_logger_history_button": "Näytä lokihistoria",
|
||||
"ui_settings_title": "Käyttöliittymän asetukset",
|
||||
"haptic_feedback_label": "Haptic Feedback",
|
||||
"haptic_feedback_label": "Haptinen palaute",
|
||||
"use_system_toasts_label": "Käytä järjestelmäpaahtoleipää",
|
||||
"updates_title": "Päivitykset",
|
||||
"auto_update_check": "Automaattipäivityksen tarkistus",
|
||||
@@ -245,14 +245,14 @@
|
||||
"choose_location_button": "Valitse sijainti",
|
||||
"manual_coordinates_hint": "Säädä koordinaatit käsin.",
|
||||
"saved_coordinates_subtitle": "Hallitse tallennettuja parof-paikkoja",
|
||||
"teleport_to_friend_button": "Teleport to Friend",
|
||||
"teleport_to_friend_button": "Teleportoi yst?v?n luo",
|
||||
"spoof_location_toggle": "Spoof- sijainti",
|
||||
"suspend_location_updates": "Keskeytä sijaintipäivitykset",
|
||||
"saved_coordinates_title": "Tallennetut koordinaatit",
|
||||
"no_saved_coordinates_hint": "Ei tallennettuja koordinaatteja",
|
||||
"delete_dialog_title": "Poista tallennettu koordinaatti",
|
||||
"delete_dialog_message": "Haluatko varmasti poistaa tämän tallennetun koordinaatin?",
|
||||
"teleport_to_friend_title": "Teleport to Friend",
|
||||
"teleport_to_friend_title": "Teleportoi yst?v?n luo",
|
||||
"search_bar": "Etsi",
|
||||
"no_friends_map": "Ei ystäviä kartalla",
|
||||
"no_friends_found": "Ei ystäviä löytynyt"
|
||||
@@ -595,7 +595,7 @@
|
||||
},
|
||||
"actions": {
|
||||
"clean_snapchat_cache": {
|
||||
"name": "Clean Snapchat Cache",
|
||||
"name": "Tyhjenn? Snapchat-v?limuisti",
|
||||
"description": "Puhdistaa Snapchat välimuistin"
|
||||
},
|
||||
"manage_friend_list": {
|
||||
@@ -1021,7 +1021,7 @@
|
||||
"friendly": "Ystävällinen",
|
||||
"humorous": "Huvittavaa",
|
||||
"empathetic": "Empaattinen",
|
||||
"toxic": "Edgy",
|
||||
"toxic": "Rohkea",
|
||||
"busy": "Kiire"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1112,7 +1112,7 @@
|
||||
"translation_position": {
|
||||
"above": "Tekstin yläpuolella",
|
||||
"below": "Alla oleva teksti",
|
||||
"inline": "Inline"
|
||||
"inline": "Riviss?"
|
||||
},
|
||||
"source_language": {
|
||||
"auto": "Tunnista automaattisesti"
|
||||
@@ -1139,7 +1139,7 @@
|
||||
"description": "Aseta paroofoidun sijainnin koordinaatit"
|
||||
},
|
||||
"walk_radius": {
|
||||
"name": "Walk Radius",
|
||||
"name": "K?velys?de",
|
||||
"description": "Satunnaisesti kävellä tämän säteen sisällä (ft)"
|
||||
},
|
||||
"always_update_location": {
|
||||
@@ -1173,7 +1173,7 @@
|
||||
"description": "Ohittaa median lataamisen laadun",
|
||||
"properties": {
|
||||
"force_video_upload_source_quality": {
|
||||
"name": "Force Video Upload Source Quality",
|
||||
"name": "Pakota videol?hetyksen l?hdelaatu",
|
||||
"description": "Pakottaa Snapchat käyttämään lähdelaatua ladattaessa videoita\nHuomaa, että tämä ei välttämättä poista metatietoja mediasta"
|
||||
},
|
||||
"disable_image_compression": {
|
||||
@@ -1210,7 +1210,7 @@
|
||||
"name": "Käyttöliittymän asetukset",
|
||||
"properties": {
|
||||
"haptic_feedback": {
|
||||
"name": "Haptic Feedback"
|
||||
"name": "Haptinen palaute"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1777,7 +1777,7 @@
|
||||
"description": "API-avain tekoälypalveluun"
|
||||
},
|
||||
"ai_system_prompt": {
|
||||
"name": "AI System Prompt",
|
||||
"name": "AI-j?rjestelm?kehote",
|
||||
"description": "Järjestelmäkehotus, joka määrittelee tekoälyn persoonallisuuden ja käyttäytymisen"
|
||||
},
|
||||
"ai_max_tokens": {
|
||||
@@ -1991,7 +1991,7 @@
|
||||
"description": "Keskeytä käännös, kun palvelu on estetty"
|
||||
},
|
||||
"max_retries": {
|
||||
"name": "Max Retries",
|
||||
"name": "Maks. uudelleenyritykset",
|
||||
"description": "Uusintayritysten enimmäismäärä"
|
||||
},
|
||||
"retry_delay": {
|
||||
@@ -2090,7 +2090,7 @@
|
||||
}
|
||||
},
|
||||
"streaks_reminder": {
|
||||
"name": "Streaks Reminder",
|
||||
"name": "Streaks-muistutus",
|
||||
"description": "Säännöllisesti ilmoittaa Streaks",
|
||||
"properties": {
|
||||
"interval": {
|
||||
@@ -2112,7 +2112,7 @@
|
||||
"description": "Kokeelliset ominaisuudet",
|
||||
"properties": {
|
||||
"native_hooks": {
|
||||
"name": "Native Hooks",
|
||||
"name": "Natiivit hookit",
|
||||
"description": "Epäturvallisia ominaisuuksia, jotka liittyvät Snapchat'n natiivi-koodiin",
|
||||
"properties": {
|
||||
"composer_hooks": {
|
||||
@@ -2180,7 +2180,7 @@
|
||||
"description": "Ohita Snapchatiin lähetetty Android-tunnus",
|
||||
"properties": {
|
||||
"spoof_android_id": {
|
||||
"name": "Spoof Android ID",
|
||||
"name": "Spoofaa Android ID",
|
||||
"description": "Ohita Snapchatille lähetetty Android-tunnus omalla arvolla"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -2260,7 +2260,7 @@
|
||||
"description": "Yritä korjata Friend Feed Menu kuin kun laite on offline se ei voi näyttää oikein"
|
||||
},
|
||||
"app_lock": {
|
||||
"name": "App Lock",
|
||||
"name": "Sovelluslukko",
|
||||
"description": "Estää pääsyn Snapchatiin ilman salasanaa",
|
||||
"properties": {
|
||||
"lock_on_resume": {
|
||||
@@ -2270,7 +2270,7 @@
|
||||
}
|
||||
},
|
||||
"infinite_story_boost": {
|
||||
"name": "Infinite Story Boost",
|
||||
"name": "Loputon Story-boost",
|
||||
"description": "Ohita Story Boost Limit -viive"
|
||||
},
|
||||
"meo_passcode_bypass": {
|
||||
@@ -2419,7 +2419,7 @@
|
||||
"media_download_source": {
|
||||
"none": "Ei ole",
|
||||
"pending": "Odotetaan",
|
||||
"chat_media": "Chat Media",
|
||||
"chat_media": "Chat-media",
|
||||
"story": "Tarina",
|
||||
"public_story": "Julkinen tarina",
|
||||
"spotlight": "Valokeila",
|
||||
@@ -2553,7 +2553,7 @@
|
||||
"deleted": "Poistettu",
|
||||
"business_accounts": "Yritystilit",
|
||||
"streaks": "Streakit",
|
||||
"non_streaks": "Non Streaks",
|
||||
"non_streaks": "Ilman streakseja",
|
||||
"followed": "Seuraava",
|
||||
"following": "Seuraava",
|
||||
"location_on_map": "Sijainti kartalla"
|
||||
@@ -2883,7 +2883,7 @@
|
||||
"translation_position": {
|
||||
"above": "Yllä",
|
||||
"below": "Alla",
|
||||
"inline": "Inline"
|
||||
"inline": "Riviss?"
|
||||
},
|
||||
"language_codes": {
|
||||
"en": "Englanti",
|
||||
@@ -3064,7 +3064,7 @@
|
||||
}
|
||||
},
|
||||
"ui_settings_title": "Käyttöliittymän asetukset",
|
||||
"haptic_feedback_label": "Haptic Feedback",
|
||||
"haptic_feedback_label": "Haptinen palaute",
|
||||
"updates_title": "Päivitykset",
|
||||
"auto_update_check": "Automaattipäivityksen tarkistus",
|
||||
"update_check_frequency_daily": "Vuorokausi",
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Sélectionner la langue",
|
||||
@@ -182,7 +182,7 @@
|
||||
"rules_title": "Règles",
|
||||
"participants_text": "{count}participants",
|
||||
"not_found": "Non trouvé",
|
||||
"streaks_title": "Streaks",
|
||||
"streaks_title": "S?ries",
|
||||
"streaks_length_text": "Longueur:{length}",
|
||||
"streaks_expiration_text": "Expire dans{eta}",
|
||||
"streaks_expiration_text_expired": "Expiré",
|
||||
@@ -532,11 +532,11 @@
|
||||
}
|
||||
},
|
||||
"auto_open_snaps": {
|
||||
"name": "Auto Open Snaps",
|
||||
"name": "Ouverture auto des Snaps",
|
||||
"description": "Automatiquement ouvre Snaps lors de leur réception",
|
||||
"options": {
|
||||
"blacklist": "Exclure de Auto Open Snaps",
|
||||
"whitelist": "Auto Open Snaps"
|
||||
"whitelist": "Ouverture auto des Snaps"
|
||||
}
|
||||
},
|
||||
"hide_friend_feed": {
|
||||
@@ -716,7 +716,7 @@
|
||||
"notifications": {
|
||||
"chat_screenshot": "Capture d'écran",
|
||||
"chat_screen_record": "Enregistrement de l'écran",
|
||||
"snap_replay": "Snap Replay",
|
||||
"snap_replay": "Relecture de Snap",
|
||||
"camera_roll_save": "Roule-photo Enregistrer",
|
||||
"chat": "Chat",
|
||||
"chat_reply": "Répondre au chat",
|
||||
@@ -909,7 +909,7 @@
|
||||
"disable_confirmation_dialogs": {
|
||||
"erase_message": "Effacer le message",
|
||||
"remove_friend": "Supprimer l'ami",
|
||||
"block_friend": "Block Friend",
|
||||
"block_friend": "Bloquer l'ami",
|
||||
"ignore_friend": "Ignorer son ami",
|
||||
"hide_friend": "Cacher l'ami",
|
||||
"hide_conversation": "Cacher la conversation",
|
||||
@@ -1021,7 +1021,7 @@
|
||||
"friendly": "Amiable",
|
||||
"humorous": "Humoreux",
|
||||
"empathetic": "Empathie",
|
||||
"toxic": "Edgy",
|
||||
"toxic": "Cinglant",
|
||||
"busy": "Occupé"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1543,7 +1543,7 @@
|
||||
"description": "Saute automatiquement au prochain Snap lors du marquage d'un Snap tel que vu.\nUtilisation en combinaison avec Mark Snap comme Bouton vu"
|
||||
},
|
||||
"loop_media_playback": {
|
||||
"name": "Loop Media Playback",
|
||||
"name": "Lecture en boucle des m?dias",
|
||||
"description": "Loops media playing lors de l'affichage Snaps / Stories"
|
||||
},
|
||||
"disable_replay_in_ff": {
|
||||
@@ -1781,7 +1781,7 @@
|
||||
"description": "Invitation système qui définit la personnalité et le comportement de l'IA"
|
||||
},
|
||||
"ai_max_tokens": {
|
||||
"name": "AI Max Tokens",
|
||||
"name": "Nombre max de tokens IA",
|
||||
"description": "Nombre maximal de jetons (mots) que l'IA peut utiliser dans les réponses"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1991,7 +1991,7 @@
|
||||
"description": "Pause traduction lorsque le service est bloqué"
|
||||
},
|
||||
"max_retries": {
|
||||
"name": "Max Retries",
|
||||
"name": "Nombre max de tentatives",
|
||||
"description": "Nombre maximal de tentatives de réessayer"
|
||||
},
|
||||
"retry_delay": {
|
||||
@@ -2552,7 +2552,7 @@
|
||||
"suggested": "Suggestion",
|
||||
"deleted": "Supprimé",
|
||||
"business_accounts": "Comptes d'entreprise",
|
||||
"streaks": "Streaks",
|
||||
"streaks": "S?ries",
|
||||
"non_streaks": "Non streaks",
|
||||
"followed": "Suivi",
|
||||
"following": "Suivant",
|
||||
@@ -2685,7 +2685,7 @@
|
||||
}
|
||||
},
|
||||
"streaks_reminder": {
|
||||
"notification_title": "Streaks",
|
||||
"notification_title": "S?ries",
|
||||
"notification_text": "Vous allez perdre votre Streak avec{friend}en{hoursLeft}heures"
|
||||
},
|
||||
"biometric_auth": {
|
||||
@@ -2720,9 +2720,9 @@
|
||||
"incoming_secret_message": "Ton ami a accepté ta clé publique. Cliquez ci-dessous pour accepter le secret."
|
||||
},
|
||||
"auto_open_snaps": {
|
||||
"title": "Auto Open Snaps",
|
||||
"title": "Ouverture auto des Snaps",
|
||||
"priority_title": "Auto Open Snaps (Priorité)",
|
||||
"error_title": "Auto Open Snaps (Errors)",
|
||||
"error_title": "Ouverture auto des Snaps (erreurs)",
|
||||
"channel_description": "Notifications pour l'ouverture automatique du statut de file d'attente snaps",
|
||||
"priority_channel_description": "Notifications hautement prioritaires pour les snaps d'ouverture automatique",
|
||||
"error_channel_description": "Notifications d'erreur lorsque l'ouverture automatique des snaps échoue",
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OuvrirRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "भाषा चुनें",
|
||||
@@ -116,7 +116,7 @@
|
||||
"reset_button": "रीसेट करें",
|
||||
"done_button": "दान",
|
||||
"clear_friend_feed": "स्पष्ट मित्र फ़ीड",
|
||||
"test_mode_label": "Enable PurrAura",
|
||||
"test_mode_label": "???????? ????? ????",
|
||||
"disable_feature_loading_label": "अक्षम सुविधा लोड हो रहा है",
|
||||
"disable_auto_mapper_label": "अक्षम ऑटो मैपर",
|
||||
"disable_bypass_indicator_label": "निष्क्रिय सूचक"
|
||||
@@ -347,9 +347,9 @@
|
||||
"no_repos_added": "कोई प्रस्ताव जोड़ा नहीं",
|
||||
"add_repo_button": "Repository जोड़ें",
|
||||
"add_repo_dialog_title": "Repository जोड़ें",
|
||||
"repo_url_label": "Repository URL",
|
||||
"repo_url_label": "?????????? URL",
|
||||
"add_button": "जोड़ें",
|
||||
"invalid_repo_title": "Invalid Repository",
|
||||
"invalid_repo_title": "?????? ??????????",
|
||||
"invalid_repo_error": "यह प्रस्ताव डेटा की आवश्यकता नहीं है।.",
|
||||
"repo_added_toast": "Repository जोड़ा",
|
||||
"add_repo_failed_toast": "प्रस्ताव को जोड़ने में विफल:{message}",
|
||||
@@ -453,9 +453,9 @@
|
||||
"no_repos_added": "कोई प्रस्ताव जोड़ा नहीं",
|
||||
"add_repo_button": "Repository जोड़ें",
|
||||
"add_repo_dialog_title": "Repository जोड़ें",
|
||||
"repo_url_label": "Repository URL",
|
||||
"repo_url_label": "?????????? URL",
|
||||
"add_button": "जोड़ें",
|
||||
"invalid_repo_title": "Invalid Repository",
|
||||
"invalid_repo_title": "?????? ??????????",
|
||||
"invalid_repo_error": "यह प्रस्ताव डेटा की आवश्यकता नहीं है।.",
|
||||
"repo_added_toast": "Repository जोड़ा",
|
||||
"add_repo_failed_toast": "प्रस्ताव को जोड़ने में विफल:{message}",
|
||||
@@ -910,7 +910,7 @@
|
||||
"erase_message": "संदेश",
|
||||
"remove_friend": "दोस्त निकालें",
|
||||
"block_friend": "ब्लॉक मित्र",
|
||||
"ignore_friend": "Ignore Friend",
|
||||
"ignore_friend": "????? ?? ?????? ????",
|
||||
"hide_friend": "मित्र",
|
||||
"hide_conversation": "छुपाना",
|
||||
"clear_conversation": "फ्रेंड फ़ीड से वार्तालाप साफ़ करें"
|
||||
@@ -1021,7 +1021,7 @@
|
||||
"friendly": "अनुकूल",
|
||||
"humorous": "विनम्र",
|
||||
"empathetic": "सहानुभूति",
|
||||
"toxic": "Edgy",
|
||||
"toxic": "????",
|
||||
"busy": "बुसी"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1429,7 +1429,7 @@
|
||||
"description": "मित्र फ़ीड से एक विशिष्ट दोस्त को छुपाएं\nइस सुविधा का प्रबंधन करने के लिए सोशल टैब का उपयोग करें"
|
||||
},
|
||||
"hide_streak_restore": {
|
||||
"name": "Hide Streak Restore",
|
||||
"name": "??????? ??????? ??????",
|
||||
"description": "मित्र फ़ीड में पुनर्स्थापित बटन छुपाएं"
|
||||
},
|
||||
"hide_quick_add_suggestions": {
|
||||
@@ -1515,11 +1515,11 @@
|
||||
"description": "किसी को यह जानने से रोकता है कि आपने अपनी कहानी फिर से देख ली है"
|
||||
},
|
||||
"hide_peek_a_peek": {
|
||||
"name": "Hide Peek-a-Peek",
|
||||
"name": "???-?-??? ??????",
|
||||
"description": "जब आप एक चैट में आधे स्वाइप करते हैं तो अधिसूचना को भेजे जाने से रोकता है"
|
||||
},
|
||||
"hide_bitmoji_presence": {
|
||||
"name": "Hide Bitmoji Presence",
|
||||
"name": "??????? ???????? ??????",
|
||||
"description": "चैट में अपने बिटमोजी को पॉप अप करने से रोकता है"
|
||||
},
|
||||
"hide_typing_notifications": {
|
||||
@@ -1581,7 +1581,7 @@
|
||||
"description": "कुछ प्रकार के संदेश भेजने से रोकता है"
|
||||
},
|
||||
"friend_mutation_notifier": {
|
||||
"name": "Friend Mutation Notifier",
|
||||
"name": "?????? ???????? ????",
|
||||
"description": "जब एक दोस्त की प्रोफ़ाइल में कुछ बदलाव आता है तो आपको सूचित करें"
|
||||
},
|
||||
"better_notifications": {
|
||||
@@ -1609,7 +1609,7 @@
|
||||
"description": "कई मीडिया संदेशों को एक पाठ अधिसूचना में जोड़ती है जब उन्हें पूर्वावलोकन नहीं किया जा सकता है। चैट पूर्वावलोकन के साथ संयोजन में उपयोग करें"
|
||||
},
|
||||
"friend_add_source": {
|
||||
"name": "Friend Add Source",
|
||||
"name": "????? ?????? ?? ?????",
|
||||
"description": "अधिसूचना में एक मित्र अनुरोध का स्रोत दिखाता है"
|
||||
},
|
||||
"reply_button": {
|
||||
@@ -1911,7 +1911,7 @@
|
||||
"description": "अगर यह विफल रहता है तो एक स्नैप खोलने के लिए समय की संख्या"
|
||||
},
|
||||
"retry_delay": {
|
||||
"name": "Retry Delay (ms)",
|
||||
"name": "??????? ???? (ms)",
|
||||
"description": "पुनः प्रयास के बीच मिलीसेकंड में विलंब"
|
||||
}
|
||||
}
|
||||
@@ -1925,7 +1925,7 @@
|
||||
"description": "ऑटो डिलीट संत संदेश को पृष्ठभूमि में चलाने की अनुमति देता है। ध्यान दें: इससे आपकी बैटरी काफी बढ़ जाएगी"
|
||||
},
|
||||
"delete_after_value": {
|
||||
"name": "Delete After (value)",
|
||||
"name": "???? ??? ????? (value)",
|
||||
"description": "भेजे गए संदेश को हटाने से पहले समय मूल्य"
|
||||
},
|
||||
"delete_after_unit": {
|
||||
@@ -1995,7 +1995,7 @@
|
||||
"description": "पुनः प्रयास की अधिकतम संख्या"
|
||||
},
|
||||
"retry_delay": {
|
||||
"name": "Retry Delay",
|
||||
"name": "??????? ????",
|
||||
"description": "Retry प्रयासों (milliseconds) के बीच विलंब"
|
||||
}
|
||||
}
|
||||
@@ -2090,7 +2090,7 @@
|
||||
}
|
||||
},
|
||||
"streaks_reminder": {
|
||||
"name": "Streaks Reminder",
|
||||
"name": "????????? ????????",
|
||||
"description": "समय-समय पर आपको अपने स्ट्रेक्स के बारे में सूचित करता है",
|
||||
"properties": {
|
||||
"interval": {
|
||||
@@ -2142,8 +2142,8 @@
|
||||
}
|
||||
},
|
||||
"disable_bitmoji": {
|
||||
"name": "Disable Bitmoji",
|
||||
"description": "Disables Friends Profile Bitmoji"
|
||||
"name": "??????? ????????? ????",
|
||||
"description": "??????? ?? ????????? ??????? ????????? ???? ??"
|
||||
},
|
||||
"custom_emoji_font": {
|
||||
"name": "कस्टम इमोजी फ़ॉन्ट",
|
||||
@@ -2180,7 +2180,7 @@
|
||||
"description": "एंड्रॉइड आईडी को स्नैपचैट में भेजा गया ओवरराइड करें",
|
||||
"properties": {
|
||||
"spoof_android_id": {
|
||||
"name": "Spoof Android ID",
|
||||
"name": "Android ID ????? ????",
|
||||
"description": "एक कस्टम मूल्य के साथ स्नैपचैट को भेजे गए एंड्रॉइड आईडी को ओवरराइड करें"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -2342,7 +2342,7 @@
|
||||
"description": "स्क्रिप्ट को स्नैपचैट में कस्टम यूआई घटकों को जोड़ने की अनुमति देता है"
|
||||
},
|
||||
"disable_log_anonymization": {
|
||||
"name": "Disable Log Anonymization",
|
||||
"name": "??? ?????????? ????????? ????",
|
||||
"description": "लॉग का नामकरण अक्षम करें"
|
||||
},
|
||||
"disable_optimization": {
|
||||
@@ -2665,7 +2665,7 @@
|
||||
},
|
||||
"select_attachments_title": "संलग्नक चुनें",
|
||||
"download_started_toast": "प्रारंभ करना",
|
||||
"unsupported_content_type_toast": "Unsupported content type!",
|
||||
"unsupported_content_type_toast": "???????? ??????? ??????!",
|
||||
"failed_no_longer_available_toast": "मीडिया अब उपलब्ध नहीं",
|
||||
"no_attachments_toast": "कोई संलग्नक नहीं मिला!",
|
||||
"already_queued_toast": "पहले से ही कतार में मीडिया!",
|
||||
@@ -2730,7 +2730,7 @@
|
||||
"processing_status": "प्रसंस्करण तस्वीरें:{queued}कतार में,{processed}प्रक्रिया",
|
||||
"monitor_status": "निगरानी...",
|
||||
"recent_snaps": "हाल ही में स्नैप",
|
||||
"action_pause": "Pause",
|
||||
"action_pause": "?????",
|
||||
"action_resume": "फिर से शुरू",
|
||||
"action_clear": "स्पष्ट कतार",
|
||||
"action_reset": "रीसेट करें",
|
||||
@@ -3078,7 +3078,7 @@
|
||||
"theme_mode_system": "प्रणाली",
|
||||
"theme_mode_light": "प्रकाश",
|
||||
"theme_mode_dark": "अंधेरा",
|
||||
"test_mode_label": "Enable PurrAura",
|
||||
"test_mode_label": "???????? ????? ????",
|
||||
"disable_feature_loading_label": "अक्षम सुविधा लोड हो रहा है",
|
||||
"disable_auto_mapper_label": "अक्षम ऑटो मैपर",
|
||||
"disable_bypass_indicator_label": "निष्क्रिय सूचक",
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Nyelv kiválasztása",
|
||||
@@ -80,11 +80,11 @@
|
||||
"debug_title": "Hibaelhárítás",
|
||||
"success_toast": "Kész!",
|
||||
"message_logger_summary": "{messageCount}üzenetek\n{storyCount}történetek",
|
||||
"export_button": "Export",
|
||||
"export_button": "Export?l?s",
|
||||
"clear_button": "Tiszta",
|
||||
"view_logger_history_button": "A bejelentkezési előzmény megtekintése",
|
||||
"ui_settings_title": "UI beállítások",
|
||||
"haptic_feedback_label": "Haptic Feedback",
|
||||
"haptic_feedback_label": "Haptikus visszajelz?s",
|
||||
"use_system_toasts_label": "Rendszerköszöntők használata",
|
||||
"updates_title": "Frissítések",
|
||||
"auto_update_check": "Automatikus frissítés ellenőrzése",
|
||||
@@ -134,8 +134,8 @@
|
||||
},
|
||||
"features": {
|
||||
"disabled": "Fogyatékos",
|
||||
"export_option": "Export",
|
||||
"import_option": "Import",
|
||||
"export_option": "Export?l?s",
|
||||
"import_option": "Import?l?s",
|
||||
"reset_option": "Újraindítás",
|
||||
"config_export_success_toast": "Config sikeresen exportált",
|
||||
"config_import_success_toast": "Sikeresen importált konfig",
|
||||
@@ -310,7 +310,7 @@
|
||||
"downloading_script": "Szkript letöltése...",
|
||||
"edit_module_button": "Szerkesztés",
|
||||
"enter_url_label": "Adja meg az URL- t",
|
||||
"import_button": "Import",
|
||||
"import_button": "Import?l?s",
|
||||
"import_from_url_button": "Importálás URL-ből",
|
||||
"import_script_from_url_title": "Szkript importálása URL-ből",
|
||||
"import_script_warning": "Csak a megbízható forrásból származó szkriptek telepítése.",
|
||||
@@ -363,7 +363,7 @@
|
||||
"logs_tab": "Bejelentkezés",
|
||||
"catalog_button": "Katalógus",
|
||||
"add_rule_button": "Cikk hozzáadása",
|
||||
"import_button": "Import",
|
||||
"import_button": "Import?l?s",
|
||||
"filters_title": "Szűrők",
|
||||
"search_by_label": "Keresés",
|
||||
"newest_first_label": "A következő",
|
||||
@@ -371,7 +371,7 @@
|
||||
"until_label": "Amíg",
|
||||
"unit_label": "Egység",
|
||||
"pick_a_date_button": "Válasszon időpontot",
|
||||
"export_button": "Export",
|
||||
"export_button": "Export?l?s",
|
||||
"delete_button": "Törlés",
|
||||
"search_placeholder": "Keresés",
|
||||
"no_logs_found": "Nem található napló",
|
||||
@@ -438,7 +438,7 @@
|
||||
},
|
||||
"friend_tracker_import": {
|
||||
"title": "Barát nyomkövető importálása",
|
||||
"confirm_button": "Import",
|
||||
"confirm_button": "Import?l?s",
|
||||
"back_button_description": "Menj vissza",
|
||||
"expand_button_description": "Kiterjesztés vagy összeomlás kategóriája",
|
||||
"imported_toast": "Behozott nyomkövető",
|
||||
@@ -479,7 +479,7 @@
|
||||
"config_import": {
|
||||
"title": "Config összefoglaló importálása",
|
||||
"back_button_description": "Menj vissza",
|
||||
"confirm_button": "Import",
|
||||
"confirm_button": "Import?l?s",
|
||||
"expand_button_description": "Kiterjesztés vagy összeomlás kategóriája",
|
||||
"enabled": "Engedélyezve",
|
||||
"disabled": "Fogyatékos",
|
||||
@@ -702,7 +702,7 @@
|
||||
"append_type": "A médiatípus hozzáadása a fájlnévhez"
|
||||
},
|
||||
"auto_download_sources": {
|
||||
"friend_snaps": "Friend Snaps",
|
||||
"friend_snaps": "Bar?t Snaps",
|
||||
"friend_stories": "Barát történetek",
|
||||
"public_stories": "Nyilvános történetek",
|
||||
"spotlight": "Spotlight"
|
||||
@@ -719,7 +719,7 @@
|
||||
"snap_replay": "Snap ismétlés",
|
||||
"camera_roll_save": "Kamera Roll Mentés",
|
||||
"chat": "Chat",
|
||||
"chat_reply": "Chat Reply",
|
||||
"chat_reply": "Chat v?lasz",
|
||||
"snap": "Snap",
|
||||
"typing": "Gépelés",
|
||||
"stories": "Történetek",
|
||||
@@ -843,7 +843,7 @@
|
||||
"ORIGINAL": "Eredeti média",
|
||||
"NOTE": "Hangjegyzet",
|
||||
"SNAP": "Snap",
|
||||
"SAVEABLE_SNAP": "Saveable Snap",
|
||||
"SAVEABLE_SNAP": "Menthet? Snap",
|
||||
"null": "Snapchat alapértelmezés"
|
||||
},
|
||||
"strip_media_metadata": {
|
||||
@@ -893,12 +893,12 @@
|
||||
"snapchat_plus": {
|
||||
"not_subscribed": "Nincs előfizetve",
|
||||
"basic": "Alap",
|
||||
"ad_free": "Ad Free",
|
||||
"ad_free": "Rekl?mmentes",
|
||||
"null": "Alapértelmezés"
|
||||
},
|
||||
"bypass_video_length_restriction": {
|
||||
"single": "Egységes média",
|
||||
"split": "Split media",
|
||||
"split": "M?dia feloszt?sa",
|
||||
"null": "Alapértelmezés"
|
||||
},
|
||||
"old_bitmoji_selfie": {
|
||||
@@ -909,7 +909,7 @@
|
||||
"disable_confirmation_dialogs": {
|
||||
"erase_message": "Üzenet törlése",
|
||||
"remove_friend": "Barát eltávolítása",
|
||||
"block_friend": "Block Friend",
|
||||
"block_friend": "Ismer?s blokkol?sa",
|
||||
"ignore_friend": "A barát figyelmen kívül hagyása",
|
||||
"hide_friend": "A barát elrejtése",
|
||||
"hide_conversation": "A beszélgetés elrejtése",
|
||||
@@ -1021,7 +1021,7 @@
|
||||
"friendly": "Barátságos",
|
||||
"humorous": "Humoros",
|
||||
"empathetic": "Empatikus",
|
||||
"toxic": "Edgy",
|
||||
"toxic": "Provokat?v",
|
||||
"busy": "Elfoglalt"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1085,7 +1085,7 @@
|
||||
"auto_reply_content_types": {
|
||||
"chat_messages": "Chat üzenetek",
|
||||
"snap_messages": "Snaps",
|
||||
"story_share_messages": "Story Shares",
|
||||
"story_share_messages": "Story megoszt?sok",
|
||||
"story_reply_messages": "Történet válaszok",
|
||||
"external_media_messages": "Külső média",
|
||||
"voice_note_messages": "Hangjegyek",
|
||||
@@ -1139,7 +1139,7 @@
|
||||
"description": "Állítsák be a hamis hely koordinátáit"
|
||||
},
|
||||
"walk_radius": {
|
||||
"name": "Walk Radius",
|
||||
"name": "S?t?l?si sug?r",
|
||||
"description": "Véletlenszerűen járkáljon ezen a sugáron belül (ft)"
|
||||
},
|
||||
"always_update_location": {
|
||||
@@ -1173,7 +1173,7 @@
|
||||
"description": "Felülírja a média feltöltési minőségét",
|
||||
"properties": {
|
||||
"force_video_upload_source_quality": {
|
||||
"name": "Force Video Upload Source Quality",
|
||||
"name": "Vide?felt?lt?s forr?smin?s?g?nek k?nyszer?t?se",
|
||||
"description": "Forces Snapchat használni a forrás minőségét feltöltésekor videók\nFelhívjuk figyelmét, hogy ez nem távolíthatja el a metaadatokat a médiából"
|
||||
},
|
||||
"disable_image_compression": {
|
||||
@@ -1210,7 +1210,7 @@
|
||||
"name": "UI beállítások",
|
||||
"properties": {
|
||||
"haptic_feedback": {
|
||||
"name": "Haptic Feedback"
|
||||
"name": "Haptikus visszajelz?s"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1223,7 +1223,7 @@
|
||||
"description": "Eltávolítja a szakaszokat a Történetek oldaláról\nSzükség lehet egy frissítésre, hogy megfelelően működjön"
|
||||
},
|
||||
"block_ads": {
|
||||
"name": "Block Ads",
|
||||
"name": "Hirdet?sek blokkol?sa",
|
||||
"description": "Megelőzi a hirdetések megjelenését"
|
||||
},
|
||||
"disable_custom_tabs": {
|
||||
@@ -1243,7 +1243,7 @@
|
||||
"description": "Megjeleníti a szerző felhasználónevét a Spotlight megjegyzésekben"
|
||||
},
|
||||
"spotlight_comments_username_icon": {
|
||||
"name": "Spotlight Comments Username Icon",
|
||||
"name": "Spotlight megjegyz?sek felhaszn?l?n?v ikon",
|
||||
"description": "Válassza ki, melyik ikon jelenik meg a felhasználónevek mellett a Spotlight megjegyzésekben"
|
||||
},
|
||||
"bypass_video_length_restriction": {
|
||||
@@ -1313,7 +1313,7 @@
|
||||
"description": "A képek elmentése egy megadott formátumban"
|
||||
},
|
||||
"force_voice_note_format": {
|
||||
"name": "Force Voice Note Format",
|
||||
"name": "Hangjegyzet form?tum k?nyszer?t?se",
|
||||
"description": "Forces Voice A megjegyzések elmentése meghatározott formátumban"
|
||||
},
|
||||
"auto_download_voice_notes": {
|
||||
@@ -1399,7 +1399,7 @@
|
||||
"description": "Megjelenít egy kis preview mellett láthatatlan Snaps a chat"
|
||||
},
|
||||
"bootstrap_override": {
|
||||
"name": "Bootstrap Override",
|
||||
"name": "Bootstrap fel?lb?r?l?s",
|
||||
"description": "A felhasználói felület bootstrap beállításainak felülbírálása",
|
||||
"properties": {
|
||||
"app_appearance": {
|
||||
@@ -1407,7 +1407,7 @@
|
||||
"description": "Állandó app megjelenés beállítása"
|
||||
},
|
||||
"home_tab": {
|
||||
"name": "Home Tab",
|
||||
"name": "Kezd?lap f?l",
|
||||
"description": "Felülírja a startup lapot a Snapchat megnyitásakor"
|
||||
}
|
||||
}
|
||||
@@ -1445,11 +1445,11 @@
|
||||
"description": "Válassza ki, melyik UI-komponenst rejtse el"
|
||||
},
|
||||
"opera_media_quick_info": {
|
||||
"name": "Opera Media Quick Info",
|
||||
"name": "Opera Media gyors inf?",
|
||||
"description": "Hasznos információkat mutat a médiáról, például a teremtés dátumát az opera nézőinek menüjében"
|
||||
},
|
||||
"old_bitmoji_selfie": {
|
||||
"name": "Old Bitmoji Selfie",
|
||||
"name": "R?gi Bitmoji szelfi",
|
||||
"description": "Visszahozza a Bitmoji selfies régebbi Snapchat verziók"
|
||||
},
|
||||
"disable_spotlight": {
|
||||
@@ -1469,7 +1469,7 @@
|
||||
"description": "Lehetővé teszi a függőleges történet néző minden történet"
|
||||
},
|
||||
"enable_friend_feed_menu_bar": {
|
||||
"name": "Friend Feed Menu Bar",
|
||||
"name": "Bar?tfolyam men?sor",
|
||||
"description": "Engedélyezi az új Friend Feed menü"
|
||||
},
|
||||
"message_indicators": {
|
||||
@@ -1511,7 +1511,7 @@
|
||||
"description": "Megelőzi, hogy bárki megtudja, hogy láttad a történetét"
|
||||
},
|
||||
"prevent_story_rewatch_indicator": {
|
||||
"name": "Prevent Story Rewatch Indicator",
|
||||
"name": "Story ?jran?z?s jelz? letilt?sa",
|
||||
"description": "Megelőzi, hogy bárki megtudja, hogy újra megnézted a sztoriját"
|
||||
},
|
||||
"hide_peek_a_peek": {
|
||||
@@ -1543,7 +1543,7 @@
|
||||
"description": "Automatikusan ugrik a következő Snap, amikor megjelöli a Snap, mint látható.\nAlkalmazása együtt Mark Snap, mint látott gomb"
|
||||
},
|
||||
"loop_media_playback": {
|
||||
"name": "Loop Media Playback",
|
||||
"name": "M?dia lej?tsz?s ism?tl?se",
|
||||
"description": "Loops média lejátszás megtekintésekor Snaps / Stories"
|
||||
},
|
||||
"disable_replay_in_ff": {
|
||||
@@ -1593,15 +1593,15 @@
|
||||
"description": "Csoportos értesítések egyetlen példányban"
|
||||
},
|
||||
"chat_preview": {
|
||||
"name": "Chat Preview",
|
||||
"name": "Chat el?n?zet",
|
||||
"description": "Megjeleníti a kapott üzenetek előnézetét a bejelentésben"
|
||||
},
|
||||
"media_preview": {
|
||||
"name": "Media Preview",
|
||||
"name": "M?dia el?n?zet",
|
||||
"description": "Megjeleníti a kiválasztott médiatípusok előnézetét a bejelentésben"
|
||||
},
|
||||
"media_caption": {
|
||||
"name": "Media Caption",
|
||||
"name": "M?dia felirat",
|
||||
"description": "Megjeleníti a média csatolását a bejelentésben"
|
||||
},
|
||||
"stacked_media_messages": {
|
||||
@@ -1761,7 +1761,7 @@
|
||||
"description": "Az MI használata intelligens automatikus válaszok generálásához sablon üzenetek helyett"
|
||||
},
|
||||
"ai_provider": {
|
||||
"name": "AI Provider",
|
||||
"name": "MI szolg?ltat?",
|
||||
"description": "Válassza ki, melyik MI szolgáltatást kell használni a válaszok generálására"
|
||||
},
|
||||
"ai_endpoint_url": {
|
||||
@@ -1781,7 +1781,7 @@
|
||||
"description": "System prompt, amely meghatározza a MI személyiségét és viselkedését"
|
||||
},
|
||||
"ai_max_tokens": {
|
||||
"name": "AI Max Tokens",
|
||||
"name": "MI max tokenek",
|
||||
"description": "A válaszokban használható MI-k maximális száma (szavak)"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1895,7 +1895,7 @@
|
||||
"description": "Lehetővé teszi az Auto Open Snaps futását a háttérben. Megjegyzés: Ez jelentősen lemeríti az akkumulátorát"
|
||||
},
|
||||
"min_delay": {
|
||||
"name": "Min Delay (ms)",
|
||||
"name": "Min. k?sleltet?s (ms)",
|
||||
"description": "Minimális késleltetés milliszekundumban a felbontás előtt"
|
||||
},
|
||||
"max_delay_ms": {
|
||||
@@ -1991,7 +1991,7 @@
|
||||
"description": "A fordítás megszakítása a szolgáltatás blokkolása esetén"
|
||||
},
|
||||
"max_retries": {
|
||||
"name": "Max Retries",
|
||||
"name": "Max. ?jrapr?b?lkoz?s",
|
||||
"description": "Az ismételt kísérletek maximális száma"
|
||||
},
|
||||
"retry_delay": {
|
||||
@@ -2094,7 +2094,7 @@
|
||||
"description": "Időszakos bejelentés a csíkjaidról",
|
||||
"properties": {
|
||||
"interval": {
|
||||
"name": "Interval",
|
||||
"name": "Intervallum",
|
||||
"description": "Az egyes emlékeztetők közötti időtartam (óra)"
|
||||
},
|
||||
"remaining_hours": {
|
||||
@@ -2112,11 +2112,11 @@
|
||||
"description": "Kísérleti jellemzők",
|
||||
"properties": {
|
||||
"native_hooks": {
|
||||
"name": "Native Hooks",
|
||||
"name": "Native hookok",
|
||||
"description": "A Snapchat anyavegyületének kódját behálózó biztonsági jellemzők",
|
||||
"properties": {
|
||||
"composer_hooks": {
|
||||
"name": "Composer Hooks",
|
||||
"name": "Composer hookok",
|
||||
"description": "Injekciós kód a Composer cross-platform UI keretébe",
|
||||
"properties": {
|
||||
"show_first_created_username": {
|
||||
@@ -2132,7 +2132,7 @@
|
||||
"description": "Több lehetőséget ad az önmegsemmisítő időzítőre a Snap küldésekor"
|
||||
},
|
||||
"composer_console": {
|
||||
"name": "Composer Console",
|
||||
"name": "Composer konzol",
|
||||
"description": "Lehetővé teszi, hogy végrehajtsa a JavaScript kódot a Composer (arm64 csak)"
|
||||
},
|
||||
"composer_logs": {
|
||||
@@ -2204,11 +2204,11 @@
|
||||
"description": "Konvertálja pattogó chat külső média helyben. Ez jelenik meg a chat letöltési környezet menü"
|
||||
},
|
||||
"media_file_picker": {
|
||||
"name": "Media File Picker",
|
||||
"name": "M?diaf?jlv?laszt?",
|
||||
"description": "Lehetővé teszi, hogy válasszon bármilyen videó / audio fájlt a galériából"
|
||||
},
|
||||
"story_logger": {
|
||||
"name": "Story Logger",
|
||||
"name": "Story napl?",
|
||||
"description": "A barátok történeteinek története"
|
||||
},
|
||||
"call_recorder": {
|
||||
@@ -2230,7 +2230,7 @@
|
||||
"description": "Javítja a hangjegy átiratot",
|
||||
"properties": {
|
||||
"force_transcription": {
|
||||
"name": "Force Voice Note Transcription",
|
||||
"name": "Hangjegyzet-?tirat k?nyszer?t?se",
|
||||
"description": "Lehetővé teszi az összes hangjegy átírását"
|
||||
},
|
||||
"preferred_transcription_lang": {
|
||||
@@ -2256,7 +2256,7 @@
|
||||
"description": "Nem kiadott / béta Snapchat funkciók engedélyezése"
|
||||
},
|
||||
"context_menu_fix": {
|
||||
"name": "Context Menu Fix",
|
||||
"name": "Kontextusmen? jav?t?s",
|
||||
"description": "A Friend Feed Menü javítása, mint amikor a készülék kikapcsolt, nem jelenik meg helyesen"
|
||||
},
|
||||
"app_lock": {
|
||||
@@ -2274,7 +2274,7 @@
|
||||
"description": "A Story Boost Limit késleltetése"
|
||||
},
|
||||
"meo_passcode_bypass": {
|
||||
"name": "My Eyes Only Passcode Bypass",
|
||||
"name": "My Eyes Only jelsz? megker?l?se",
|
||||
"description": "A Szemeim megkerülése Csak jelszó\nEz csak akkor fog működni, ha a jelszó helyesen van bejegyezve, mielőtt"
|
||||
},
|
||||
"no_friend_score_delay": {
|
||||
@@ -2407,7 +2407,7 @@
|
||||
"MAP_REACTION": "Térkép Reakció",
|
||||
"chat_messages": "Chat üzenetek",
|
||||
"snap_messages": "Snaps",
|
||||
"story_share_messages": "Story Shares",
|
||||
"story_share_messages": "Story megoszt?sok",
|
||||
"story_reply_messages": "Történet válaszok",
|
||||
"external_media_messages": "Külső média",
|
||||
"voice_note_messages": "Hangjegy",
|
||||
@@ -2419,12 +2419,12 @@
|
||||
"media_download_source": {
|
||||
"none": "Nincs",
|
||||
"pending": "Függőben",
|
||||
"chat_media": "Chat Media",
|
||||
"chat_media": "Chat m?dia",
|
||||
"story": "Történet",
|
||||
"public_story": "Nyilvános történet",
|
||||
"spotlight": "Spotlight",
|
||||
"profile_picture": "Profilkép",
|
||||
"story_logger": "Story Logger",
|
||||
"story_logger": "Story napl?",
|
||||
"message_logger": "Üzenetjelző",
|
||||
"merged": "Egyesülve",
|
||||
"voice_call": "Hanghívás"
|
||||
@@ -2454,7 +2454,7 @@
|
||||
"ORIGINAL": "Eredeti média",
|
||||
"NOTE": "Hangjegyzet",
|
||||
"SNAP": "Snap",
|
||||
"SAVEABLE_SNAP": "Saveable Snap",
|
||||
"SAVEABLE_SNAP": "Menthet? Snap",
|
||||
"null": "Snapchat alapértelmezés",
|
||||
"multiple_media_toast": "Egyszerre csak egy médiát küldhetsz"
|
||||
},
|
||||
@@ -2514,7 +2514,7 @@
|
||||
"actions": {
|
||||
"remove_friends": "A barátok eltávolítása",
|
||||
"clear_conversations": "Beszélgetések törlése",
|
||||
"clear_friend_feed": "Clear Friend Feed ({count})",
|
||||
"clear_friend_feed": "Bar?tfolyam t?rl?se ({count})",
|
||||
"unfollow": "Nem követi",
|
||||
"remove": "Eltávolítás"
|
||||
},
|
||||
@@ -2580,7 +2580,7 @@
|
||||
"download_medias_title": "Média letöltése"
|
||||
},
|
||||
"dialog_negative_button": "Törlés",
|
||||
"dialog_positive_button": "Export",
|
||||
"dialog_positive_button": "Export?l?s",
|
||||
"exported_to": "Kivitel{path}",
|
||||
"exporting_chats": "Chats exportálása...",
|
||||
"processing_chats": "Felfeldolgozás{amount}beszélgetések...",
|
||||
@@ -2627,7 +2627,7 @@
|
||||
},
|
||||
"cleared_from_feed": "Takarmányból megtisztítva",
|
||||
"tracker_actions": {
|
||||
"log": "Log",
|
||||
"log": "Napl?",
|
||||
"in_app_notification": "Az alkalmazás bejelentése",
|
||||
"push_notification": "Értesítés",
|
||||
"custom": "Egyéni"
|
||||
@@ -2721,7 +2721,7 @@
|
||||
},
|
||||
"auto_open_snaps": {
|
||||
"title": "Automatikus nyitás",
|
||||
"priority_title": "Auto Open Snaps (Priority)",
|
||||
"priority_title": "Snaps automatikus megnyit?sa (priorit?s)",
|
||||
"error_title": "Auto Open Snaps (hibák)",
|
||||
"channel_description": "Értesítések az automatikus nyitási csatolású csatolások sorban állásáról",
|
||||
"priority_channel_description": "Az automatikus nyitásra vonatkozó kiemelt értesítések",
|
||||
@@ -2751,7 +2751,7 @@
|
||||
"unknown_user": "Ismeretlen felhasználó",
|
||||
"content_type_external_media": "Külső média",
|
||||
"content_type_snap": "Snap",
|
||||
"conversation_type_friend_dm": "Friend DM",
|
||||
"conversation_type_friend_dm": "Bar?t DM",
|
||||
"conversation_type_dm": "DM",
|
||||
"conversation_type_group_chat": "Csoport",
|
||||
"conversation_type_chat": "Chat",
|
||||
@@ -2925,7 +2925,7 @@
|
||||
"rules": "Szabályok"
|
||||
},
|
||||
"actions": {
|
||||
"export": "Export",
|
||||
"export": "Export?l?s",
|
||||
"delete": "Törlés",
|
||||
"add_rule": "Cikk hozzáadása",
|
||||
"save_rule": "A szabály mentése"
|
||||
@@ -3053,7 +3053,7 @@
|
||||
"config_json": "Konfigurációs fájl",
|
||||
"mappings_json": "Mappings fájl",
|
||||
"message_logger_db": "Üzenet bejelentkezési adatbázis",
|
||||
"pinned_best_friend_txt": "Pinned Best Friend File",
|
||||
"pinned_best_friend_txt": "Kit?z?tt legjobb bar?t f?jl",
|
||||
"native_sig_cache_txt": "Natív aláírás Cache fájl"
|
||||
},
|
||||
"settings": {
|
||||
@@ -3064,7 +3064,7 @@
|
||||
}
|
||||
},
|
||||
"ui_settings_title": "UI beállítások",
|
||||
"haptic_feedback_label": "Haptic Feedback",
|
||||
"haptic_feedback_label": "Haptikus visszajelz?s",
|
||||
"updates_title": "Frissítések",
|
||||
"auto_update_check": "Automatikus frissítés ellenőrzése",
|
||||
"update_check_frequency_daily": "Napi",
|
||||
@@ -3097,7 +3097,7 @@
|
||||
"sort_by_folder": "Rendezés mappa szerint",
|
||||
"include_my_eyes_only": "Csak a szemeimet tartalmazza",
|
||||
"cancel": "Törlés",
|
||||
"export": "Export",
|
||||
"export": "Export?l?s",
|
||||
"quit": "Kilépés",
|
||||
"done": "Kész",
|
||||
"ok": "OKÉ",
|
||||
@@ -3111,7 +3111,7 @@
|
||||
"import_script_from_url": "Szkript importálása URL-ből",
|
||||
"warning_imported_scripts": "Figyelem: Az importált szkriptek károsak lehetnek az eszközre. Csak megbízható forrásból származó forgatókönyveket importálunk.",
|
||||
"enter_url_here": "Adja meg az URL-t:",
|
||||
"import": "Import",
|
||||
"import": "Import?l?s",
|
||||
"cancel": "Törlés",
|
||||
"documentation": "Dokumentáció"
|
||||
},
|
||||
@@ -3170,7 +3170,7 @@
|
||||
},
|
||||
"debug_dialogs": {
|
||||
"info": "Információ",
|
||||
"refs": "Refs",
|
||||
"refs": "Referenci?k",
|
||||
"arroyo": "Arroyo",
|
||||
"message": "Üzenet",
|
||||
"media_references": "Médiahivatkozások",
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Pilih Bahasa",
|
||||
@@ -52,7 +52,7 @@
|
||||
"customize_bottom_bar_subtitle": "Pilih tab mana yang ditampilkan pada layar rumah Anda",
|
||||
"available_tabs_title": "Tab Tersedia",
|
||||
"shown_tabs_title": "Tab Shown",
|
||||
"reset_button": "Reset",
|
||||
"reset_button": "Atur ulang",
|
||||
"done_button": "Selesai"
|
||||
},
|
||||
"sections": {
|
||||
@@ -84,13 +84,13 @@
|
||||
"clear_button": "Hapus",
|
||||
"view_logger_history_button": "Tilik Riwayat Logger",
|
||||
"ui_settings_title": "Pengaturan UI",
|
||||
"haptic_feedback_label": "Haptic Feedback",
|
||||
"haptic_feedback_label": "Umpan balik haptik",
|
||||
"use_system_toasts_label": "Gunakan Kaus Sistem",
|
||||
"updates_title": "Pemutakhiran",
|
||||
"auto_update_check": "Periksa Perbarui Otomatis",
|
||||
"update_check_frequency_daily": "Harian",
|
||||
"update_check_frequency_weekly": "Mingguan",
|
||||
"update_check_frequency_monthly": "Monthly",
|
||||
"update_check_frequency_monthly": "Bulanan",
|
||||
"update_channel_stable": "Stabil",
|
||||
"update_channel_prerelease": "Lepaskan",
|
||||
"update_notification_channel_name": "Pemutakhiran",
|
||||
@@ -107,13 +107,13 @@
|
||||
"friend_notes_no_notes_to_backup": "Belum ada catatan cadangan",
|
||||
"friend_notes_backup_success": "Catatan teman didukung",
|
||||
"friend_notes_restore_success": "Catatan teman dipulihkan",
|
||||
"backup_button": "Backup",
|
||||
"backup_button": "Cadangan",
|
||||
"restore_button": "Pulihkan",
|
||||
"view_button": "Tilik",
|
||||
"customize_bottom_bar_title": "Ubahan Batang Bawah",
|
||||
"customize_bottom_bar_subtitle": "Pilih tab mana yang ditampilkan pada layar rumah Anda",
|
||||
"available_tabs_title": "Tab Tersedia",
|
||||
"reset_button": "Reset",
|
||||
"reset_button": "Atur ulang",
|
||||
"done_button": "Selesai",
|
||||
"clear_friend_feed": "Hapus Pakan Teman",
|
||||
"test_mode_label": "Aktifkan PurrAura",
|
||||
@@ -136,7 +136,7 @@
|
||||
"disabled": "Dinonaktifkan",
|
||||
"export_option": "Ekspor",
|
||||
"import_option": "Impor",
|
||||
"reset_option": "Reset",
|
||||
"reset_option": "Atur ulang",
|
||||
"config_export_success_toast": "Konfigurasi diekspor dengan sukses",
|
||||
"config_import_success_toast": "Konfigurasi diimpor dengan sukses",
|
||||
"config_import_failure_toast": "Gagal mengimpor konfigurasi{error}",
|
||||
@@ -369,7 +369,7 @@
|
||||
"newest_first_label": "Terbaru pertama",
|
||||
"since_label": "Sejak",
|
||||
"until_label": "Sampai",
|
||||
"unit_label": "Unit",
|
||||
"unit_label": "Satuan",
|
||||
"pick_a_date_button": "Pilih tanggal",
|
||||
"export_button": "Ekspor",
|
||||
"delete_button": "Hapus",
|
||||
@@ -385,7 +385,7 @@
|
||||
"rule_name_label": "Nama Aturan",
|
||||
"default_rule_name": "Aturan Baru",
|
||||
"author_name_label": "Penulis",
|
||||
"scope_section_title": "Scope",
|
||||
"scope_section_title": "Lingkup",
|
||||
"scope_all": "Semua Teman / Grup",
|
||||
"scope_whitelist": "Tidak ada kecuali",
|
||||
"scope_blacklist": "Semua orang kecuali",
|
||||
@@ -394,7 +394,7 @@
|
||||
"no_events_text": "Belum ada kejadian yang ditambahkan",
|
||||
"add_event_dialog_title": "Tambah Peristiwa",
|
||||
"event_type_label": "Jenis Kejadian",
|
||||
"triggers_title": "Triggers",
|
||||
"triggers_title": "Pemicu",
|
||||
"conditions_title": "Kondisi",
|
||||
"condition_only_inside_conversation": "Hanya ketika aku di dalam percakapan",
|
||||
"condition_only_outside_conversation": "Hanya ketika aku di luar percakapan",
|
||||
@@ -511,7 +511,7 @@
|
||||
"name": "Mode Stealth",
|
||||
"description": "Mencegah siapa pun dari mengetahui Anda telah membuka Snaps mereka / Chats dan percakapan",
|
||||
"options": {
|
||||
"blacklist": "Exclude from Stealth Mode",
|
||||
"blacklist": "Kecualikan dari mode siluman",
|
||||
"whitelist": "Mode siluman"
|
||||
}
|
||||
},
|
||||
@@ -608,7 +608,7 @@
|
||||
},
|
||||
"export_memories": {
|
||||
"name": "Ekspor Kenangan",
|
||||
"description": "Exports memories into a ZIP file"
|
||||
"description": "Ekspor Memories ke file ZIP"
|
||||
},
|
||||
"bulk_messaging_action": {
|
||||
"name": "Aksi Pesan Bolak",
|
||||
@@ -628,7 +628,7 @@
|
||||
},
|
||||
"friend_tracker": {
|
||||
"name": "Pelacak Teman",
|
||||
"description": "Track your friends on Snapchat"
|
||||
"description": "Lacak teman Anda di Snapchat"
|
||||
},
|
||||
"logger_history": {
|
||||
"name": "Riwayat Logger",
|
||||
@@ -716,7 +716,7 @@
|
||||
"notifications": {
|
||||
"chat_screenshot": "Cuplikan layar",
|
||||
"chat_screen_record": "Catatan Layar",
|
||||
"snap_replay": "Snap Replay",
|
||||
"snap_replay": "Putar ulang Snap",
|
||||
"camera_roll_save": "Kamera Roll Save",
|
||||
"chat": "Obrolan",
|
||||
"chat_reply": "Balas Percakapan",
|
||||
@@ -733,43 +733,43 @@
|
||||
"map_live_location": "Lokasi Peta Hidup"
|
||||
},
|
||||
"auto_read": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Daftar hitam",
|
||||
"whitelist": "Daftar putih",
|
||||
"disabled": "Dinonaktifkan"
|
||||
},
|
||||
"hide_typing_indicator": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Daftar hitam",
|
||||
"whitelist": "Daftar putih",
|
||||
"disabled": "Dinonaktifkan"
|
||||
},
|
||||
"auto_delete_sent_messages": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Daftar hitam",
|
||||
"whitelist": "Daftar putih",
|
||||
"disabled": "Dinonaktifkan"
|
||||
},
|
||||
"auto_download": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Daftar hitam",
|
||||
"whitelist": "Daftar putih",
|
||||
"disabled": "Dinonaktifkan"
|
||||
},
|
||||
"stealth": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Daftar hitam",
|
||||
"whitelist": "Daftar putih",
|
||||
"disabled": "Dinonaktifkan"
|
||||
},
|
||||
"auto_save": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Daftar hitam",
|
||||
"whitelist": "Daftar putih",
|
||||
"disabled": "Dinonaktifkan"
|
||||
},
|
||||
"message_logger": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Daftar hitam",
|
||||
"whitelist": "Daftar putih",
|
||||
"disabled": "Dinonaktifkan"
|
||||
},
|
||||
"auto_reply": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Daftar hitam",
|
||||
"whitelist": "Daftar putih",
|
||||
"disabled": "Dinonaktifkan"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -1021,7 +1021,7 @@
|
||||
"friendly": "Ramah",
|
||||
"humorous": "Humor",
|
||||
"empathetic": "Empatik",
|
||||
"toxic": "Edgy",
|
||||
"toxic": "Kasar",
|
||||
"busy": "Sibuk"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1086,7 +1086,7 @@
|
||||
"chat_messages": "Pesan Percakapan",
|
||||
"snap_messages": "Snaps",
|
||||
"story_share_messages": "Tabungan Cerita",
|
||||
"story_reply_messages": "Story Replies",
|
||||
"story_reply_messages": "Balasan Story",
|
||||
"external_media_messages": "Media Eksternal",
|
||||
"voice_note_messages": "Catatan Suara",
|
||||
"sticker_messages": "Stiker",
|
||||
@@ -1112,7 +1112,7 @@
|
||||
"translation_position": {
|
||||
"above": "Di atas teks",
|
||||
"below": "Di bawah teks",
|
||||
"inline": "Inline"
|
||||
"inline": "Sebaris"
|
||||
},
|
||||
"source_language": {
|
||||
"auto": "Mendeteksi otomatis"
|
||||
@@ -1210,7 +1210,7 @@
|
||||
"name": "Pengaturan UI",
|
||||
"properties": {
|
||||
"haptic_feedback": {
|
||||
"name": "Haptic Feedback"
|
||||
"name": "Umpan balik haptik"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1337,11 +1337,11 @@
|
||||
"description": "Nyatakan tambahan Pilihan FFmpeg",
|
||||
"properties": {
|
||||
"threads": {
|
||||
"name": "Threads",
|
||||
"name": "Utas",
|
||||
"description": "Jumlah thread yang dipakai"
|
||||
},
|
||||
"preset": {
|
||||
"name": "Preset",
|
||||
"name": "Prasetel",
|
||||
"description": "Set kecepatan konversi"
|
||||
},
|
||||
"constant_rate_factor": {
|
||||
@@ -1367,7 +1367,7 @@
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"name": "Logging",
|
||||
"name": "Pencatatan",
|
||||
"description": "Tampilkan toast ketika media mengunduh"
|
||||
},
|
||||
"custom_path_format": {
|
||||
@@ -1389,7 +1389,7 @@
|
||||
"description": "Tampilkan pratilik pesan terakhir dalam Pangsa Teman",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"name": "Amount",
|
||||
"name": "Jumlah",
|
||||
"description": "Jumlah pesan yang akan ditampilkan"
|
||||
}
|
||||
}
|
||||
@@ -1399,7 +1399,7 @@
|
||||
"description": "Tampilkan pratinjau kecil di samping Snaps tak terlihat dalam percakapan"
|
||||
},
|
||||
"bootstrap_override": {
|
||||
"name": "Bootstrap Override",
|
||||
"name": "Penggantian bootstrap",
|
||||
"description": "Timpa pengaturan bootstrap antar muka pengguna",
|
||||
"properties": {
|
||||
"app_appearance": {
|
||||
@@ -1507,7 +1507,7 @@
|
||||
"description": "Mencegah Snapchat mendeteksi ketika Anda mengambil cuplikan layar"
|
||||
},
|
||||
"anonymous_story_viewing": {
|
||||
"name": "Anonymous Story Viewing",
|
||||
"name": "Melihat Story secara anonim",
|
||||
"description": "Mencegah siapa pun dari mengetahui Anda telah melihat cerita mereka"
|
||||
},
|
||||
"prevent_story_rewatch_indicator": {
|
||||
@@ -1617,7 +1617,7 @@
|
||||
"description": "Tambahkan tombol balasan ke pemberitahuan"
|
||||
},
|
||||
"smart_replies": {
|
||||
"name": "Smart Replies",
|
||||
"name": "Balasan pintar",
|
||||
"description": "Tambahkan balasan yang disarankan ke pemberitahuan (Android 10 +). Gunakan dalam kombinasi dengan Tombol Balas"
|
||||
},
|
||||
"download_button": {
|
||||
@@ -1693,7 +1693,7 @@
|
||||
"description": "Membuat catatan audio tidak dapat diselamatkan"
|
||||
},
|
||||
"story_reply": {
|
||||
"name": "Story Replies",
|
||||
"name": "Balasan Story",
|
||||
"description": "Membuat cerita menjawab tak terbalas"
|
||||
}
|
||||
}
|
||||
@@ -1713,7 +1713,7 @@
|
||||
}
|
||||
},
|
||||
"strip_media_metadata": {
|
||||
"name": "Strip Media Metadata",
|
||||
"name": "Hapus metadata media",
|
||||
"description": "Hapus metadata media sebelum mengirim pesan"
|
||||
},
|
||||
"bypass_message_retention_policy": {
|
||||
@@ -1847,11 +1847,11 @@
|
||||
"description": "Pesan jawaban otomatis untuk pesan obrolan teks"
|
||||
},
|
||||
"snap_messages": {
|
||||
"name": "Snap Replies",
|
||||
"name": "Balasan Snap",
|
||||
"description": "Pesan jawaban otomatis untuk terkunci"
|
||||
},
|
||||
"story_share_messages": {
|
||||
"name": "Story Share Replies",
|
||||
"name": "Balasan berbagi Story",
|
||||
"description": "Pesan jawaban otomatis untuk berbagi cerita"
|
||||
},
|
||||
"story_reply_messages": {
|
||||
@@ -1867,11 +1867,11 @@
|
||||
"description": "Pesan jawaban otomatis untuk catatan suara"
|
||||
},
|
||||
"sticker_messages": {
|
||||
"name": "Sticker Replies",
|
||||
"name": "Balasan stiker",
|
||||
"description": "Pesan jawaban otomatis bagi stiker"
|
||||
},
|
||||
"tiny_snap_messages": {
|
||||
"name": "Tiny Snap Replies",
|
||||
"name": "Balasan Snap kecil",
|
||||
"description": "Pesan jawaban otomatis untuk kecil terkunci"
|
||||
},
|
||||
"map_reaction_messages": {
|
||||
@@ -2090,7 +2090,7 @@
|
||||
}
|
||||
},
|
||||
"streaks_reminder": {
|
||||
"name": "Streaks Reminder",
|
||||
"name": "Pengingat streak",
|
||||
"description": "Secara periodikal memberitahu Anda tentang Streaks Anda",
|
||||
"properties": {
|
||||
"interval": {
|
||||
@@ -2128,7 +2128,7 @@
|
||||
"description": "Meningkatkan jumlah media maksimum Anda dapat mengirim dari gulungan kamera"
|
||||
},
|
||||
"custom_self_destruct_snap_delay": {
|
||||
"name": "Custom Self Destruct Snap Delay",
|
||||
"name": "Penundaan Snap hancur sendiri khusus",
|
||||
"description": "Memberikan lebih banyak pilihan untuk timer penghancuran diri sendiri ketika mengirim Snap"
|
||||
},
|
||||
"composer_console": {
|
||||
@@ -2177,7 +2177,7 @@
|
||||
},
|
||||
"spoof_device_id": {
|
||||
"name": "ID Perangkat Spoof",
|
||||
"description": "Override the Android ID sent to Snapchat",
|
||||
"description": "Ganti ID Android yang dikirim ke Snapchat",
|
||||
"properties": {
|
||||
"spoof_android_id": {
|
||||
"name": "ID Android Spoof",
|
||||
@@ -2264,7 +2264,7 @@
|
||||
"description": "Mencegah akses ke Snapchat tanpa kode akses",
|
||||
"properties": {
|
||||
"lock_on_resume": {
|
||||
"name": "Lock On Resume",
|
||||
"name": "Kunci saat dilanjutkan",
|
||||
"description": "Kunci aplikasi ketika dibuka kembali"
|
||||
}
|
||||
}
|
||||
@@ -2274,7 +2274,7 @@
|
||||
"description": "Melewati penundaan Batas Boost Cerita"
|
||||
},
|
||||
"meo_passcode_bypass": {
|
||||
"name": "My Eyes Only Passcode Bypass",
|
||||
"name": "Lewati kode sandi My Eyes Only",
|
||||
"description": "Melewati Mataku Hanya kode sandi\nIni hanya akan bekerja jika kode akses telah dimasukkan dengan benar sebelum"
|
||||
},
|
||||
"no_friend_score_delay": {
|
||||
@@ -2282,7 +2282,7 @@
|
||||
"description": "Hapus penundaan ketika melihat Skor Teman"
|
||||
},
|
||||
"best_friend_pinning": {
|
||||
"name": "Best Friend Pinning",
|
||||
"name": "Penyematan teman terbaik",
|
||||
"description": "Memungkinkan Anda untuk pin teman sebagai nomor satu teman terbaik Anda. Catatan: Hanya Anda dapat melihat teman terbaik Anda terjepit"
|
||||
},
|
||||
"e2ee": {
|
||||
@@ -2401,18 +2401,18 @@
|
||||
"FAMILY_CENTER_INVITE": "Undang Pusat Keluarga",
|
||||
"FAMILY_CENTER_ACCEPT": "Family Center Terima",
|
||||
"FAMILY_CENTER_LEAVE": "Meninggalkan Pusat Keluarga",
|
||||
"STATUS_PLUS_GIFT": "Status Plus Gift",
|
||||
"TINY_SNAP": "Tiny Snap",
|
||||
"STATUS_PLUS_GIFT": "Hadiah Status Plus",
|
||||
"TINY_SNAP": "Snap kecil",
|
||||
"STATUS_COUNTDOWN": "Hitung mundur",
|
||||
"MAP_REACTION": "Reaksi Peta",
|
||||
"chat_messages": "Pesan Percakapan",
|
||||
"snap_messages": "Snaps",
|
||||
"story_share_messages": "Tabungan Cerita",
|
||||
"story_reply_messages": "Story Replies",
|
||||
"story_reply_messages": "Balasan Story",
|
||||
"external_media_messages": "Media Eksternal",
|
||||
"voice_note_messages": "Catatan Suara",
|
||||
"sticker_messages": "Sticker",
|
||||
"tiny_snap_messages": "Tiny Snap",
|
||||
"tiny_snap_messages": "Snap kecil",
|
||||
"map_reaction_messages": "Reaksi Peta",
|
||||
"half_swipes": "Setengah Berenang"
|
||||
},
|
||||
@@ -2493,7 +2493,7 @@
|
||||
"not_subscribed": "Tidak Berlangganan"
|
||||
},
|
||||
"friendship_link_type": {
|
||||
"mutual": "Mutual",
|
||||
"mutual": "Saling",
|
||||
"outgoing": "Keluar",
|
||||
"blocked": "Diblokir",
|
||||
"deleted": "Dihapus",
|
||||
@@ -2553,7 +2553,7 @@
|
||||
"deleted": "Dihapus",
|
||||
"business_accounts": "Akun Bisnis",
|
||||
"streaks": "Streaks",
|
||||
"non_streaks": "Non Streaks",
|
||||
"non_streaks": "Tanpa streak",
|
||||
"followed": "Diikuti",
|
||||
"following": "Mengikuti",
|
||||
"location_on_map": "Lokasi pada Peta"
|
||||
@@ -2582,7 +2582,7 @@
|
||||
"dialog_negative_button": "Batal",
|
||||
"dialog_positive_button": "Ekspor",
|
||||
"exported_to": "Exported to{path}",
|
||||
"exporting_chats": "Exporting Chats...",
|
||||
"exporting_chats": "Mengekspor chat...",
|
||||
"processing_chats": "Proses{amount}percakapan...",
|
||||
"export_fail": "Gagal mengekspor percakapan{conversation}",
|
||||
"writing_output": "Menulis keluaran...",
|
||||
@@ -2622,12 +2622,12 @@
|
||||
"snap_replayed": "Snap Diputar Ulang",
|
||||
"snap_replayed_twice": "Snap Replayed Dua kali",
|
||||
"snap_screenshot": "Cuplikan Snap",
|
||||
"snap_screen_record": "Snap Screen Record",
|
||||
"i_can_see_you": "I Can See You"
|
||||
"snap_screen_record": "Rekam layar Snap",
|
||||
"i_can_see_you": "Aku bisa melihatmu"
|
||||
},
|
||||
"cleared_from_feed": "Dibersihkan dari pakan",
|
||||
"tracker_actions": {
|
||||
"log": "Log",
|
||||
"log": "Catat",
|
||||
"in_app_notification": "Pemberitahuan In- App",
|
||||
"push_notification": "Pemberitahuan Push",
|
||||
"custom": "Gubahan"
|
||||
@@ -2759,7 +2759,7 @@
|
||||
"notification_statistics": "STATIK",
|
||||
"notification_queue_size": "Ukuran Antrian",
|
||||
"notification_total_opened": "Total Snaps dibuka",
|
||||
"notification_queue_preview": "QUEUE PREVIEW",
|
||||
"notification_queue_preview": "PRATINJAU ANTRIAN",
|
||||
"notification_processing_continue": "Proses akan dilanjutkan secara otomatis...",
|
||||
"notification_no_snaps_queue": "Tidak terkunci dalam antrian.",
|
||||
"notification_queue_cleared_opened": "Antrian dibersihkan ({opened}dibuka)",
|
||||
@@ -2883,7 +2883,7 @@
|
||||
"translation_position": {
|
||||
"above": "Atas",
|
||||
"below": "Bawah",
|
||||
"inline": "Inline"
|
||||
"inline": "Sebaris"
|
||||
},
|
||||
"language_codes": {
|
||||
"en": "Inggris",
|
||||
@@ -2915,7 +2915,7 @@
|
||||
"et": "Estonia",
|
||||
"lv": "Latvia",
|
||||
"lt": "Lithuania",
|
||||
"mt": "Maltese",
|
||||
"mt": "Malta",
|
||||
"ga": "Irlandia",
|
||||
"cy": "Welsh"
|
||||
},
|
||||
@@ -3028,11 +3028,11 @@
|
||||
},
|
||||
"edit_rule": {
|
||||
"custom_rule": "Aturan Gubahan",
|
||||
"scope": "Scope",
|
||||
"scope": "Lingkup",
|
||||
"events": "Kejadian",
|
||||
"add_event": "Tambah Peristiwa",
|
||||
"type": "Tipe",
|
||||
"triggers": "Triggers",
|
||||
"triggers": "Pemicu",
|
||||
"conditions": "Kondisi",
|
||||
"only_inside_conversation": "Hanya ketika aku di dalam percakapan",
|
||||
"only_outside_conversation": "Hanya ketika aku di luar percakapan",
|
||||
@@ -3064,12 +3064,12 @@
|
||||
}
|
||||
},
|
||||
"ui_settings_title": "Pengaturan UI",
|
||||
"haptic_feedback_label": "Haptic Feedback",
|
||||
"haptic_feedback_label": "Umpan balik haptik",
|
||||
"updates_title": "Pemutakhiran",
|
||||
"auto_update_check": "Periksa Perbarui Otomatis",
|
||||
"update_check_frequency_daily": "Harian",
|
||||
"update_check_frequency_weekly": "Mingguan",
|
||||
"update_check_frequency_monthly": "Monthly",
|
||||
"update_check_frequency_monthly": "Bulanan",
|
||||
"update_channel_stable": "Stabil",
|
||||
"update_channel_prerelease": "Lepaskan",
|
||||
"friend_notes_title": "Catatan Teman",
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Seleziona la lingua",
|
||||
@@ -30,10 +30,10 @@
|
||||
"home": "Home",
|
||||
"home_about": "Informazioni",
|
||||
"home_settings": "Impostazioni impostazioni",
|
||||
"home_logs": "Logs",
|
||||
"home_logs": "Registri",
|
||||
"logger_history": "Storia di Logger",
|
||||
"logged_stories": "Storie intasate",
|
||||
"friend_tracker": "Friend Tracker",
|
||||
"friend_tracker": "Tracker amici",
|
||||
"friend_tracker_catalog": "Catalogo Tracker Friend",
|
||||
"manage_friend_tracker_repos": "Gestire i repository di Friend Tracker",
|
||||
"edit_rule": "Modifica",
|
||||
@@ -42,7 +42,7 @@
|
||||
"social": "Sociale",
|
||||
"manage_scope": "Gestisci lo Scope",
|
||||
"messaging_preview": "Anteprima",
|
||||
"scripts": "Scripts",
|
||||
"scripts": "Script",
|
||||
"manage_script_repos": "Gestione dei repository Script",
|
||||
"view_logger_history": "Storia di Logger",
|
||||
"better_location": "Migliore posizione"
|
||||
@@ -123,7 +123,7 @@
|
||||
},
|
||||
"tasks": {
|
||||
"no_tasks": "Nessuna attività",
|
||||
"merge_button": "Merge",
|
||||
"merge_button": "Unisci",
|
||||
"failed_to_open_file": "Non ha aperto il file",
|
||||
"merge_files_toast": "Merito{count}file",
|
||||
"remove_selected_tasks_title": "Sei sicuro di voler rimuovere le attività selezionate?",
|
||||
@@ -276,7 +276,7 @@
|
||||
"content": "PurrfectSnap include uno strumento di scripting, consentendo l'esecuzione del codice definito dall'utente sul dispositivo. Utilizzare estrema cautela e installare solo moduli da fonti conosciute e affidabili. I moduli non autorizzati o non verificati possono costituire rischi di sicurezza per il sistema."
|
||||
},
|
||||
"reset_config": {
|
||||
"title": "Reset config",
|
||||
"title": "Reimposta configurazione",
|
||||
"content": "Sei sicuro di voler resettare la configurazione?",
|
||||
"success_toast": "Ripristino configurazione con successo"
|
||||
},
|
||||
@@ -305,7 +305,7 @@
|
||||
"clear_module_data_failed": "Non riuscita a cancellare i dati del modulo",
|
||||
"delete_module_button": "Cancella",
|
||||
"delete_module_failed": "Non è stato possibile eliminare il modulo",
|
||||
"documentation_button": "Docs",
|
||||
"documentation_button": "Documentazione",
|
||||
"download_script_failed": "Non è riuscito a scaricare lo script",
|
||||
"downloading_script": "Scarica lo script...",
|
||||
"edit_module_button": "Modifica",
|
||||
@@ -360,7 +360,7 @@
|
||||
},
|
||||
"friend_tracker": {
|
||||
"rules_tab": "Regole",
|
||||
"logs_tab": "Logs",
|
||||
"logs_tab": "Registri",
|
||||
"catalog_button": "Catalogo",
|
||||
"add_rule_button": "Aggiungere la regola",
|
||||
"import_button": "Importazioni",
|
||||
@@ -394,7 +394,7 @@
|
||||
"no_events_text": "Nessun evento aggiunto",
|
||||
"add_event_dialog_title": "Aggiungi evento",
|
||||
"event_type_label": "Tipo Evento",
|
||||
"triggers_title": "Triggers",
|
||||
"triggers_title": "Trigger",
|
||||
"conditions_title": "Condizioni",
|
||||
"condition_only_inside_conversation": "Solo quando sono dentro la conversazione",
|
||||
"condition_only_outside_conversation": "Solo quando sono fuori a parlare",
|
||||
@@ -500,7 +500,7 @@
|
||||
},
|
||||
"properties": {
|
||||
"auto_download": {
|
||||
"name": "Auto download",
|
||||
"name": "Download automatico",
|
||||
"description": "Scaricare automaticamente Snaps quando li visualizza",
|
||||
"options": {
|
||||
"blacklist": "Escluso da Auto Download",
|
||||
@@ -532,11 +532,11 @@
|
||||
}
|
||||
},
|
||||
"auto_open_snaps": {
|
||||
"name": "Auto Open Snaps",
|
||||
"name": "Apri Snaps automaticamente",
|
||||
"description": "Apre automaticamente Snaps quando li riceve",
|
||||
"options": {
|
||||
"blacklist": "Escluso da Auto Open Snaps",
|
||||
"whitelist": "Auto Open Snaps"
|
||||
"whitelist": "Apri Snaps automaticamente"
|
||||
}
|
||||
},
|
||||
"hide_friend_feed": {
|
||||
@@ -627,7 +627,7 @@
|
||||
"description": "Importa i file da usare in Snapchat"
|
||||
},
|
||||
"friend_tracker": {
|
||||
"name": "Friend Tracker",
|
||||
"name": "Tracker amici",
|
||||
"description": "Traccia i tuoi amici su Snapchat"
|
||||
},
|
||||
"logger_history": {
|
||||
@@ -659,7 +659,7 @@
|
||||
"null": "Utilizzare il livello reale della batteria"
|
||||
},
|
||||
"friend_feed_menu_buttons": {
|
||||
"auto_download": "⬇️ Auto Download",
|
||||
"auto_download": "?? Download automatico",
|
||||
"auto_save": "💬 Salva automaticamente i messaggi",
|
||||
"unsaveable_messages": "⬇️ Messaggi non rivelabili",
|
||||
"auto_open_snaps": "📷 Auto a scatto aperto",
|
||||
@@ -671,7 +671,7 @@
|
||||
"conversation_info": "Informazioni sulla conversazione",
|
||||
"e2e_encryption": "🔒 Utilizzare la crittografia E2E",
|
||||
"message_logger": "Traduzione:",
|
||||
"auto_read": "✅ Auto Read",
|
||||
"auto_read": "? Lettura automatica",
|
||||
"hide_typing_indicator": "🙈 Nascondi Indicatore di digitazione"
|
||||
},
|
||||
"schedule_scheduled_for": "Programmato per{name}in{time}",
|
||||
@@ -734,42 +734,42 @@
|
||||
},
|
||||
"auto_read": {
|
||||
"blacklist": "Lista nera",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista consentiti",
|
||||
"disabled": "Disabili"
|
||||
},
|
||||
"hide_typing_indicator": {
|
||||
"blacklist": "Lista nera",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista consentiti",
|
||||
"disabled": "Disabili"
|
||||
},
|
||||
"auto_delete_sent_messages": {
|
||||
"blacklist": "Lista nera",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista consentiti",
|
||||
"disabled": "Disabili"
|
||||
},
|
||||
"auto_download": {
|
||||
"blacklist": "Lista nera",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista consentiti",
|
||||
"disabled": "Disabili"
|
||||
},
|
||||
"stealth": {
|
||||
"blacklist": "Lista nera",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista consentiti",
|
||||
"disabled": "Disabili"
|
||||
},
|
||||
"auto_save": {
|
||||
"blacklist": "Lista nera",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista consentiti",
|
||||
"disabled": "Disabili"
|
||||
},
|
||||
"message_logger": {
|
||||
"blacklist": "Lista nera",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista consentiti",
|
||||
"disabled": "Disabili"
|
||||
},
|
||||
"auto_reply": {
|
||||
"blacklist": "Lista nera",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista consentiti",
|
||||
"disabled": "Disabili"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -792,7 +792,7 @@
|
||||
"null": "Utilizzare Snapchat predefinito"
|
||||
},
|
||||
"custom_emoji_font": {
|
||||
"null": "Default Emoji Font"
|
||||
"null": "Font emoji predefinito"
|
||||
},
|
||||
"custom_shared_library": {
|
||||
"null": "Utilizzare la libreria predefinita"
|
||||
@@ -888,7 +888,7 @@
|
||||
"null": "Automatico"
|
||||
},
|
||||
"update_check_frequency": {
|
||||
"null": "Auto"
|
||||
"null": "Automatico"
|
||||
},
|
||||
"snapchat_plus": {
|
||||
"not_subscribed": "Non sottoscritto",
|
||||
@@ -909,7 +909,7 @@
|
||||
"disable_confirmation_dialogs": {
|
||||
"erase_message": "Cancellare il messaggio",
|
||||
"remove_friend": "Rimuovi il tuo amico",
|
||||
"block_friend": "Block Friend",
|
||||
"block_friend": "Blocca amico",
|
||||
"ignore_friend": "Ignore amico",
|
||||
"hide_friend": "Nascondi amico",
|
||||
"hide_conversation": "Nascondi Conversazione",
|
||||
@@ -917,7 +917,7 @@
|
||||
},
|
||||
"edit_text_override": {
|
||||
"multi_line_chat_input": "Linea Multi Input di chat",
|
||||
"bypass_text_input_limit": "Bypass Text Input Limit"
|
||||
"bypass_text_input_limit": "Bypass limite input testo"
|
||||
},
|
||||
"auto_purge": {
|
||||
"never": "Mai",
|
||||
@@ -954,7 +954,7 @@
|
||||
"read_media_video": "Leggi i media Video",
|
||||
"camera": "Macchina fotografica",
|
||||
"microphone": "Microfono",
|
||||
"location": "Location",
|
||||
"location": "Posizione",
|
||||
"read_contacts": "Leggi i contatti",
|
||||
"nearby_devices": "Dispositivi vicini",
|
||||
"phone_calls": "Chiamate telefoniche"
|
||||
@@ -1019,16 +1019,16 @@
|
||||
"casual": "Casuale",
|
||||
"formal": "Forma",
|
||||
"friendly": "Amichevole",
|
||||
"humorous": "Humorous",
|
||||
"empathetic": "Empathetic",
|
||||
"toxic": "Edgy",
|
||||
"humorous": "Spiritoso",
|
||||
"empathetic": "Empatico",
|
||||
"toxic": "Tagliente",
|
||||
"busy": "Occupato"
|
||||
},
|
||||
"ai_temperature": {
|
||||
"0.7": "Equilibrato (0.7)"
|
||||
},
|
||||
"ai_response_language": {
|
||||
"auto": "Auto",
|
||||
"auto": "Automatico",
|
||||
"en": "Inglese",
|
||||
"es": "Spagnolo",
|
||||
"fr": "Francese",
|
||||
@@ -1090,7 +1090,7 @@
|
||||
"external_media_messages": "Supporti esterni",
|
||||
"voice_note_messages": "Note vocali",
|
||||
"sticker_messages": "Adesivi",
|
||||
"tiny_snap_messages": "Tiny Snaps",
|
||||
"tiny_snap_messages": "Snaps piccoli",
|
||||
"map_reaction_messages": "Reazioni della mappa",
|
||||
"half_swipes": "Mezzo Swipes"
|
||||
},
|
||||
@@ -1170,7 +1170,7 @@
|
||||
},
|
||||
"media_upload_quality": {
|
||||
"name": "Qualità di caricamento dei media",
|
||||
"description": "Overrides the media upload quality",
|
||||
"description": "Sovrascrive la qualit? di caricamento dei media",
|
||||
"properties": {
|
||||
"force_video_upload_source_quality": {
|
||||
"name": "Forza Video Upload Sorgente Qualità",
|
||||
@@ -1255,7 +1255,7 @@
|
||||
"description": "Imposta la velocità predefinita per la riproduzione di video\nIl valore deve essere tra 0,1 e 4.0"
|
||||
},
|
||||
"video_playback_rate_slider": {
|
||||
"name": "Video Playback Rate Slider",
|
||||
"name": "Slider velocit? riproduzione video",
|
||||
"description": "Aggiunge un cursore nel menu contestuale dell'opera per cambiare la velocità di riproduzione video\nNota: Le modifiche si applicano solo ai video successivi"
|
||||
},
|
||||
"disable_google_play_dialogs": {
|
||||
@@ -1341,7 +1341,7 @@
|
||||
"description": "La quantità di fili da usare"
|
||||
},
|
||||
"preset": {
|
||||
"name": "Preset",
|
||||
"name": "Preimpostazione",
|
||||
"description": "Impostare la velocità della conversione"
|
||||
},
|
||||
"constant_rate_factor": {
|
||||
@@ -1349,7 +1349,7 @@
|
||||
"description": "Impostare il fattore di tasso costante per l'encoder video\nDa 0 a 51 per libx264"
|
||||
},
|
||||
"video_bitrate": {
|
||||
"name": "Video Bitrate",
|
||||
"name": "Bitrate video",
|
||||
"description": "Impostare il bitrate video (kbps)"
|
||||
},
|
||||
"audio_bitrate": {
|
||||
@@ -1399,7 +1399,7 @@
|
||||
"description": "Visualizza una piccola anteprima accanto a unseen Snaps in chat"
|
||||
},
|
||||
"bootstrap_override": {
|
||||
"name": "Bootstrap Override",
|
||||
"name": "Override Bootstrap",
|
||||
"description": "Sovrascrive le impostazioni dell'interfaccia utente",
|
||||
"properties": {
|
||||
"app_appearance": {
|
||||
@@ -1407,7 +1407,7 @@
|
||||
"description": "Imposta un aspetto persistente"
|
||||
},
|
||||
"home_tab": {
|
||||
"name": "Home Tab",
|
||||
"name": "Scheda Home",
|
||||
"description": "Sovrascrive la scheda di avvio quando si apre Snapchat"
|
||||
}
|
||||
}
|
||||
@@ -1449,7 +1449,7 @@
|
||||
"description": "Mostra informazioni utili sui media come la data di creazione nel menu contestuale del visualizzatore d'opera"
|
||||
},
|
||||
"old_bitmoji_selfie": {
|
||||
"name": "Old Bitmoji Selfie",
|
||||
"name": "Vecchio selfie Bitmoji",
|
||||
"description": "Riporta i selfie Bitmoji dalle versioni precedenti di Snapchat"
|
||||
},
|
||||
"disable_spotlight": {
|
||||
@@ -1469,7 +1469,7 @@
|
||||
"description": "Abilita lo spettatore di storia verticale per tutte le storie"
|
||||
},
|
||||
"enable_friend_feed_menu_bar": {
|
||||
"name": "Friend Feed Menu Bar",
|
||||
"name": "Barra menu del feed amici",
|
||||
"description": "Abilita il nuovo Friend Feed Menu Bar"
|
||||
},
|
||||
"message_indicators": {
|
||||
@@ -1499,11 +1499,11 @@
|
||||
}
|
||||
},
|
||||
"messaging": {
|
||||
"name": "Messaging",
|
||||
"name": "Messaggistica",
|
||||
"description": "Cambia come interagire con gli amici",
|
||||
"properties": {
|
||||
"bypass_screenshot_detection": {
|
||||
"name": "Bypass Screenshot Detection",
|
||||
"name": "Bypass rilevamento screenshot",
|
||||
"description": "Impedisce Snapchat di rilevare quando si prende uno screenshot"
|
||||
},
|
||||
"anonymous_story_viewing": {
|
||||
@@ -1543,7 +1543,7 @@
|
||||
"description": "Salta automaticamente al prossimo Snap quando si marca uno Snap come visto.\nUtilizzare in combinazione con Mark Snap come pulsante di visualizzazione"
|
||||
},
|
||||
"loop_media_playback": {
|
||||
"name": "Loop Media Playback",
|
||||
"name": "Riproduzione media in loop",
|
||||
"description": "Loops riproduzione media quando si visualizza Snaps / Stories"
|
||||
},
|
||||
"disable_replay_in_ff": {
|
||||
@@ -1647,7 +1647,7 @@
|
||||
"description": "Impedisce ai propri messaggi di essere cancellati"
|
||||
},
|
||||
"auto_purge": {
|
||||
"name": "Auto Purge",
|
||||
"name": "Pulizia automatica",
|
||||
"description": "Elimina automaticamente i messaggi memorizzati nella cache che sono più vecchi della quantità di tempo specificata"
|
||||
},
|
||||
"message_filter": {
|
||||
@@ -1717,7 +1717,7 @@
|
||||
"description": "Rimuove i metadati dei media prima di inviare come messaggio"
|
||||
},
|
||||
"bypass_message_retention_policy": {
|
||||
"name": "Bypass Message Retention Policy",
|
||||
"name": "Bypass policy di conservazione messaggi",
|
||||
"description": "Previene che i messaggi vengano cancellati dopo averli visualizzati"
|
||||
},
|
||||
"bypass_message_action_restrictions": {
|
||||
@@ -1761,7 +1761,7 @@
|
||||
"description": "Utilizzare AI per generare auto-risposte intelligenti invece di messaggi di modello"
|
||||
},
|
||||
"ai_provider": {
|
||||
"name": "AI Provider",
|
||||
"name": "Provider IA",
|
||||
"description": "Selezionare quale servizio AI da utilizzare per generare risposte"
|
||||
},
|
||||
"ai_endpoint_url": {
|
||||
@@ -1773,7 +1773,7 @@
|
||||
"description": "Modello AI da usare per generare risposte (ad esempio, gpt-3.5-turbo, gpt-4)"
|
||||
},
|
||||
"ai_api_key": {
|
||||
"name": "AI API Key",
|
||||
"name": "Chiave API IA",
|
||||
"description": "Chiave API per l'autenticazione con il servizio AI"
|
||||
},
|
||||
"ai_system_prompt": {
|
||||
@@ -1781,7 +1781,7 @@
|
||||
"description": "Il prompt del sistema che definisce la personalità e il comportamento dell'AI"
|
||||
},
|
||||
"ai_max_tokens": {
|
||||
"name": "AI Max Tokens",
|
||||
"name": "Token massimi IA",
|
||||
"description": "Numero massimo di gettoni (parole) che l'IA può usare in risposte"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1895,11 +1895,11 @@
|
||||
"description": "Consente a Auto Open Snaps di eseguire in background. Nota: Questo scolicherà significativamente la batteria"
|
||||
},
|
||||
"min_delay": {
|
||||
"name": "Min Delay (ms)",
|
||||
"name": "Ritardo minimo (ms)",
|
||||
"description": "Ritardo minimo in millisecondi prima di aprire uno scatto"
|
||||
},
|
||||
"max_delay_ms": {
|
||||
"name": "Max Delay (ms)",
|
||||
"name": "Ritardo massimo (ms)",
|
||||
"description": "Ritardo massimo in millisecondi prima di aprire uno scatto"
|
||||
},
|
||||
"queue_size": {
|
||||
@@ -2124,7 +2124,7 @@
|
||||
"description": "Mostra il primo nome utente creato accanto al nome utente corrente nella pagina del profilo"
|
||||
},
|
||||
"bypass_camera_roll_limit": {
|
||||
"name": "Bypass Camera Roll Limit",
|
||||
"name": "Bypass limite rullino",
|
||||
"description": "Aumenta la quantità massima di supporti che puoi inviare dal rullo della fotocamera"
|
||||
},
|
||||
"custom_self_destruct_snap_delay": {
|
||||
@@ -2136,7 +2136,7 @@
|
||||
"description": "Consente di eseguire il codice JavaScript in Composer (solo Arm64)"
|
||||
},
|
||||
"composer_logs": {
|
||||
"name": "Composer Logs",
|
||||
"name": "Log di Composer",
|
||||
"description": "Reindirizza i registri delle console di Composer a PurrfectSnap"
|
||||
}
|
||||
}
|
||||
@@ -2161,7 +2161,7 @@
|
||||
"properties": {
|
||||
"play_store_installer_package_name": {
|
||||
"name": "Nome del pacchetto dell'installatore di Play Store",
|
||||
"description": "Overrides the installer package name to com.android.vending"
|
||||
"description": "Sovrascrive il nome del pacchetto di installazione a com.android.vending"
|
||||
},
|
||||
"remove_vpn_transport_flag": {
|
||||
"name": "Rimuovere VPN Bandiera di trasporto",
|
||||
@@ -2256,7 +2256,7 @@
|
||||
"description": "Consente funzioni Snapchat non avviate/beta"
|
||||
},
|
||||
"context_menu_fix": {
|
||||
"name": "Context Menu Fix",
|
||||
"name": "Correzione menu contestuale",
|
||||
"description": "Tentare di riparare il menu di alimentazione Friend come quando il dispositivo è offline non può essere visualizzato correttamente"
|
||||
},
|
||||
"app_lock": {
|
||||
@@ -2270,8 +2270,8 @@
|
||||
}
|
||||
},
|
||||
"infinite_story_boost": {
|
||||
"name": "Infinite Story Boost",
|
||||
"description": "Bypass the Story Boost Limit delay"
|
||||
"name": "Potenziamento story infinito",
|
||||
"description": "Bypass ritardo limite Story Boost"
|
||||
},
|
||||
"meo_passcode_bypass": {
|
||||
"name": "Solo i miei occhi passano il codice",
|
||||
@@ -2322,7 +2322,7 @@
|
||||
}
|
||||
},
|
||||
"scripting": {
|
||||
"name": "Scripting",
|
||||
"name": "Script",
|
||||
"description": "Eseguire script personalizzati per estendere PurrfectSnap",
|
||||
"properties": {
|
||||
"developer_mode": {
|
||||
@@ -2352,7 +2352,7 @@
|
||||
}
|
||||
},
|
||||
"friend_tracker": {
|
||||
"name": "Friend Tracker",
|
||||
"name": "Tracker amici",
|
||||
"description": "Registra l'attività dell'amico su Snapchat",
|
||||
"properties": {
|
||||
"record_messaging_events": {
|
||||
@@ -2364,7 +2364,7 @@
|
||||
"description": "Consente al tracker di eseguire in background. Nota: Questo scolicherà significativamente la batteria"
|
||||
},
|
||||
"auto_purge": {
|
||||
"name": "Auto Purge",
|
||||
"name": "Pulizia automatica",
|
||||
"description": "Elimina automaticamente gli eventi cache che sono più vecchi della quantità di tempo specificata"
|
||||
}
|
||||
}
|
||||
@@ -2390,7 +2390,7 @@
|
||||
"STICKER": "Adesivi Sticker visualizzare",
|
||||
"SHARE": "Condividi",
|
||||
"STATUS": "Stato",
|
||||
"LOCATION": "Location",
|
||||
"LOCATION": "Posizione",
|
||||
"STATUS_SAVE_TO_CAMERA_ROLL": "Salvato a rullo della fotocamera",
|
||||
"STATUS_CONVERSATION_CAPTURE_SCREENSHOT": "Immagine dello schermo",
|
||||
"STATUS_CONVERSATION_CAPTURE_RECORD": "Registrazione schermo",
|
||||
@@ -2419,7 +2419,7 @@
|
||||
"media_download_source": {
|
||||
"none": "Nessuno",
|
||||
"pending": "Finanziamenti",
|
||||
"chat_media": "Chat Media",
|
||||
"chat_media": "Media chat",
|
||||
"story": "Storia",
|
||||
"public_story": "Storia pubblica",
|
||||
"spotlight": "Faretto",
|
||||
@@ -2553,7 +2553,7 @@
|
||||
"deleted": "Cancellato",
|
||||
"business_accounts": "Conti aziendali",
|
||||
"streaks": "Righe",
|
||||
"non_streaks": "Non Streaks",
|
||||
"non_streaks": "Senza sequenze",
|
||||
"followed": "Seguito",
|
||||
"following": "Seguito",
|
||||
"location_on_map": "Posizione sulla mappa"
|
||||
@@ -2619,15 +2619,15 @@
|
||||
"message_reaction_add": "Reazione del messaggio Aggiungi",
|
||||
"message_reaction_remove": "Rimozione dei messaggi",
|
||||
"snap_opened": "Aperta di scatto",
|
||||
"snap_replayed": "Snap Replayed",
|
||||
"snap_replayed_twice": "Snap Replayed Twice",
|
||||
"snap_screenshot": "Snap Screenshot",
|
||||
"snap_replayed": "Snap riprodotto",
|
||||
"snap_replayed_twice": "Snap riprodotto due volte",
|
||||
"snap_screenshot": "Screenshot di Snap",
|
||||
"snap_screen_record": "Registrazione dello schermo di scatto",
|
||||
"i_can_see_you": "Posso vederti"
|
||||
},
|
||||
"cleared_from_feed": "Cancellato da alimentazione",
|
||||
"tracker_actions": {
|
||||
"log": "Log",
|
||||
"log": "Registra",
|
||||
"in_app_notification": "Notifica dell'applicazione",
|
||||
"push_notification": "Spingere la notifica",
|
||||
"custom": "Personale"
|
||||
@@ -2720,8 +2720,8 @@
|
||||
"incoming_secret_message": "Il tuo amico ha appena accettato la chiave pubblica. Clicca qui sotto per accettare il segreto."
|
||||
},
|
||||
"auto_open_snaps": {
|
||||
"title": "Auto Open Snaps",
|
||||
"priority_title": "Auto Open Snaps (Priority)",
|
||||
"title": "Apri Snaps automaticamente",
|
||||
"priority_title": "Apri Snaps automaticamente (priorit?)",
|
||||
"error_title": "Auto Open Snaps (Errori)",
|
||||
"channel_description": "Notifiche per l'apertura automatica dello stato della coda di scatto",
|
||||
"priority_channel_description": "Notifiche ad alta priorità per l'apertura automatica",
|
||||
@@ -2736,15 +2736,15 @@
|
||||
"action_reset": "Conteggio di ripristino",
|
||||
"error_content": "Non ha aperto lo snap da{sender}:{error}",
|
||||
"resumed_feedback": "Auto aperta ripresa",
|
||||
"paused_feedback": "Auto Open Paused",
|
||||
"paused_feedback": "Apertura automatica in pausa",
|
||||
"resumed_message": "L'elaborazione continuerà automaticamente per gli snap in coda",
|
||||
"paused_message": "L'elaborazione è interrotta. Coda conservata ({count})",
|
||||
"status_paused": "Paused",
|
||||
"status_paused": "In pausa",
|
||||
"status_monitoring": "Monitoraggio",
|
||||
"status_active": "Attivo",
|
||||
"queue_cleared": "Azzeramento delle statistiche",
|
||||
"queue_cleared_title": "Queue schiarito",
|
||||
"queue_cleared_reset": "Queue Cleared & Reset",
|
||||
"queue_cleared_reset": "Coda cancellata e reimpostata",
|
||||
"queue_cleared_feedback": "Cancellato{count}snap in coda • Ripristino{processed}conteggio elaborato",
|
||||
"queue_cleared_feedback_simple": "Ripristino{processed}conteggio elaborato",
|
||||
"unknown_sender": "Sconosciuto",
|
||||
@@ -2805,7 +2805,7 @@
|
||||
"delete_rule_description": "Sei sicuro di voler eliminare questa regola?",
|
||||
"rule_name": "Nome della regola",
|
||||
"friend_tracker_notifications": {
|
||||
"notification_channel_name": "Friend Tracker",
|
||||
"notification_channel_name": "Tracker amici",
|
||||
"notification_title": "Attività di amico",
|
||||
"conversation_enter": "{friend}iscritto{conversation}",
|
||||
"conversation_exit": "{friend}sinistra{conversation}",
|
||||
@@ -2830,7 +2830,7 @@
|
||||
"i_can_see_you": "{friend}attività{conversation}:{details}"
|
||||
},
|
||||
"friend_mutation_observer": {
|
||||
"notification_channel_name": "Friend Mutation Observer",
|
||||
"notification_channel_name": "Osservatore mutazioni amici",
|
||||
"friend_removed": "{username}ti ha rimosso come amico",
|
||||
"birthday_removed": "{username}ha rimosso il loro compleanno ({birthday})",
|
||||
"birthday_added": "{username}ha aggiunto il loro compleanno ({birthday})",
|
||||
@@ -2845,7 +2845,7 @@
|
||||
"date_range_picker_end_headline": "A",
|
||||
"date_range_picker_title": "Seleziona l'intervallo di date",
|
||||
"date_picker_switch_to_calendar_mode": "Calendario",
|
||||
"date_picker_switch_to_input_mode": "Input",
|
||||
"date_picker_switch_to_input_mode": "Inserimento",
|
||||
"date_range_picker_scroll_to_previous_month": "Precedente mese",
|
||||
"date_range_picker_scroll_to_next_month": "Il mese prossimo",
|
||||
"date_picker_today_description": "Oggi",
|
||||
@@ -2921,7 +2921,7 @@
|
||||
},
|
||||
"tracker": {
|
||||
"tabs": {
|
||||
"logs": "Logs",
|
||||
"logs": "Registri",
|
||||
"rules": "Regole"
|
||||
},
|
||||
"actions": {
|
||||
@@ -3018,7 +3018,7 @@
|
||||
"message_reaction_add": "aggiunto una reazione",
|
||||
"message_reaction_remove": "rimosso una reazione",
|
||||
"snap_opened": "aperto uno scatto",
|
||||
"snap_replayed": "replayed a snap",
|
||||
"snap_replayed": "ha riprodotto uno snap",
|
||||
"snap_replayed_twice": "replayed a snap due volte",
|
||||
"snap_screenshot": "ha preso uno screenshot",
|
||||
"snap_screen_record": "schermo registrato",
|
||||
@@ -3032,7 +3032,7 @@
|
||||
"events": "Eventi",
|
||||
"add_event": "Aggiungi evento",
|
||||
"type": "Tipo",
|
||||
"triggers": "Triggers",
|
||||
"triggers": "Trigger",
|
||||
"conditions": "Condizioni",
|
||||
"only_inside_conversation": "Solo quando sono dentro la conversazione",
|
||||
"only_outside_conversation": "Solo quando sono fuori a parlare",
|
||||
@@ -3051,7 +3051,7 @@
|
||||
"clear": "Libero",
|
||||
"files": {
|
||||
"config_json": "File di configurazione",
|
||||
"mappings_json": "Mappings File",
|
||||
"mappings_json": "File di mappatura",
|
||||
"message_logger_db": "Database del Registratore di messaggi",
|
||||
"pinned_best_friend_txt": "File migliore amico Pinned",
|
||||
"native_sig_cache_txt": "Firma nativa File Cache"
|
||||
@@ -3170,7 +3170,7 @@
|
||||
},
|
||||
"debug_dialogs": {
|
||||
"info": "Info",
|
||||
"refs": "Refs",
|
||||
"refs": "Riferimenti",
|
||||
"arroyo": "Arroyo",
|
||||
"message": "Messaggio",
|
||||
"media_references": "Riferimenti",
|
||||
@@ -3185,8 +3185,8 @@
|
||||
"casual": "Casuale",
|
||||
"formal": "Forma",
|
||||
"friendly": "Amichevole",
|
||||
"humorous": "Humorous",
|
||||
"empathetic": "Empathetic",
|
||||
"humorous": "Spiritoso",
|
||||
"empathetic": "Empatico",
|
||||
"busy": "Occupato",
|
||||
"toxic": "Tossico"
|
||||
},
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3304
common/src/main/assets/lang/ku_KU.json
Normal file
3304
common/src/main/assets/lang/ku_KU.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Izvēlieties valodu",
|
||||
@@ -663,16 +663,16 @@
|
||||
"auto_save": "י Automātiski saglabāt vēstules",
|
||||
"unsaveable_messages": " Nesaglabājamas vēstules",
|
||||
"auto_open_snaps": "Snaps automātiska atvēršana",
|
||||
"stealth": "👻 Stealth Mode",
|
||||
"stealth": "?? Slepenais re??ms",
|
||||
"auto_reply": "י Automātiska atbilde",
|
||||
"auto_delete_sent_messages": "- Automātiski izdzēst nosūtītās vēstules",
|
||||
"mark_snaps_as_seen": "י Atzīmēt momentus, kā redzams",
|
||||
"mark_stories_as_seen_locally": "י Atzīmēt stāstus, kā redzams uz vietas",
|
||||
"conversation_info": "👤 Conversation Info",
|
||||
"conversation_info": "?? Sarunas inform?cija",
|
||||
"e2e_encryption": "E2E šifrēšana",
|
||||
"message_logger": "📝 Message Logger",
|
||||
"message_logger": "?? Zi?ojumu ?urn?ls",
|
||||
"auto_read": "י Automātiski lasīt",
|
||||
"hide_typing_indicator": "🙈 Hide Typing Indicator"
|
||||
"hide_typing_indicator": "?? Pasl?pt rakst??anas indikatoru"
|
||||
},
|
||||
"schedule_scheduled_for": "Plānots{name}skaits{time}",
|
||||
"schedule_sending_in": "Sūta{time}",
|
||||
@@ -1021,7 +1021,7 @@
|
||||
"friendly": "Draudzīgi",
|
||||
"humorous": "Mitrs",
|
||||
"empathetic": "Empātiski",
|
||||
"toxic": "Edgy",
|
||||
"toxic": "Asu",
|
||||
"busy": "Aizņemts"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1050,7 +1050,7 @@
|
||||
"fi": "Somu"
|
||||
},
|
||||
"friendGreeting": {
|
||||
"Hey": "Hey"
|
||||
"Hey": "Sveiks"
|
||||
},
|
||||
"half_swipe_messages": {
|
||||
"[\"I noticed you half-swiped! I'll respond soon.\"]": "Es pamanīju, ka tu esi puspeldējis! Es drīz atbildēšu."
|
||||
@@ -1477,7 +1477,7 @@
|
||||
"description": "Pievieno ziņojumiem specifisku rādītāju ikonas\nPiezīme: Rādītāji var nebūt 100% precīzi"
|
||||
},
|
||||
"stealth_mode_indicator": {
|
||||
"name": "Stealth Mode indikators",
|
||||
"name": "Slepen? re??ma indikators",
|
||||
"description": "Papildina emoji blakus sarunām slēptā režīmā"
|
||||
},
|
||||
"edit_text_override": {
|
||||
@@ -1536,7 +1536,7 @@
|
||||
},
|
||||
"mark_snap_as_seen_button": {
|
||||
"name": "Atzīmēt kā meklēto pogu",
|
||||
"description": "Pievieno pogu, lai atzīmētu Snap kā redzams, to aplūkojot.\nTas darbosies pat tad, ja Stealth Mode ir ieslēgts"
|
||||
"description": "Pievieno pogu, lai atz?m?tu Snap k? redz?tu, to apl?kojot.\nTas darbosies pat tad, ja slepenais re??ms ir iesl?gts"
|
||||
},
|
||||
"skip_when_marking_as_seen": {
|
||||
"name": "Izlaist, kad iezīmēt kā redzētu",
|
||||
@@ -2156,7 +2156,7 @@
|
||||
}
|
||||
},
|
||||
"spoof": {
|
||||
"name": "Spoof",
|
||||
"name": "Vilto?ana",
|
||||
"description": "Spoof dažādu informāciju par jums",
|
||||
"properties": {
|
||||
"play_store_installer_package_name": {
|
||||
@@ -2176,11 +2176,11 @@
|
||||
"description": "Piespiest tīkla transportu ziņot Wi-Fi mobilo datu vietā"
|
||||
},
|
||||
"spoof_device_id": {
|
||||
"name": "Spoof Device ID",
|
||||
"name": "Viltot ier?ces ID",
|
||||
"description": "Aizstāt uz Snapchat nosūtīto Android ID",
|
||||
"properties": {
|
||||
"spoof_android_id": {
|
||||
"name": "Spoof Android ID",
|
||||
"name": "Viltot Android ID",
|
||||
"description": "Aizstāt Android ID nosūtīts uz Snapchat ar pielāgotu vērtību"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -2553,7 +2553,7 @@
|
||||
"deleted": "Dzēsts",
|
||||
"business_accounts": "Uzņēmējdarbības konti",
|
||||
"streaks": "Streaks",
|
||||
"non_streaks": "Non Streaks",
|
||||
"non_streaks": "Bez streakiem",
|
||||
"followed": "Seko",
|
||||
"following": "Pēc",
|
||||
"location_on_map": "Vieta uz kartes"
|
||||
@@ -2759,7 +2759,7 @@
|
||||
"notification_statistics": "STATISTIKA",
|
||||
"notification_queue_size": "Rindas izmērs",
|
||||
"notification_total_opened": "Atvērti visi momenti",
|
||||
"notification_queue_preview": "QUEUE PREVIEW",
|
||||
"notification_queue_preview": "RINDAS PRIEK?SKATS",
|
||||
"notification_processing_continue": "Apstrāde tiks turpināta automātiski...",
|
||||
"notification_no_snaps_queue": "Snaps rindā.",
|
||||
"notification_queue_cleared_opened": "Rinda noskaidrota ({opened}atvērts)",
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Velg språk",
|
||||
@@ -58,7 +58,7 @@
|
||||
"sections": {
|
||||
"home": {
|
||||
"version_title": "v{versionName}Av evig",
|
||||
"update_title": "PurrfectSnap Update",
|
||||
"update_title": "Oppdatering av PurrfectSnap",
|
||||
"update_content": "Utgave{version}er tilgjengelig!",
|
||||
"update_button": "Last ned",
|
||||
"debug_build_summary_title": "Du kjører en feilsøkingsbygging av PurrfectSnap",
|
||||
@@ -84,14 +84,14 @@
|
||||
"clear_button": "Fjern",
|
||||
"view_logger_history_button": "Vis Logger Historie",
|
||||
"ui_settings_title": "UI-innstillinger",
|
||||
"haptic_feedback_label": "Haptic Feedback",
|
||||
"haptic_feedback_label": "Haptisk tilbakemelding",
|
||||
"use_system_toasts_label": "Bruk System Toasts",
|
||||
"updates_title": "Oppdateringer",
|
||||
"auto_update_check": "Automatisk oppdateringskontroll",
|
||||
"update_check_frequency_daily": "Daglig",
|
||||
"update_check_frequency_weekly": "Uken",
|
||||
"update_check_frequency_monthly": "Månedlig",
|
||||
"update_channel_stable": "Stable",
|
||||
"update_channel_stable": "Stabil",
|
||||
"update_channel_prerelease": "Forutsetning",
|
||||
"update_notification_channel_name": "Oppdateringer",
|
||||
"update_notification_channel_description": "Bli varslet når nye utgivelser er tilgjengelige",
|
||||
@@ -240,7 +240,7 @@
|
||||
"save_coordinates_dialog_title": "Lagre koordinater",
|
||||
"saved_name_dialog_hint": "Lagret navn",
|
||||
"latitude_dialog_hint": "Lengdegrad",
|
||||
"longitude_dialog_hint": "Longitude",
|
||||
"longitude_dialog_hint": "Lengdegrad",
|
||||
"save_dialog_button": "Lagre",
|
||||
"choose_location_button": "Velg et sted",
|
||||
"manual_coordinates_hint": "Finjuster koordinatene manuelt.",
|
||||
@@ -305,7 +305,7 @@
|
||||
"clear_module_data_failed": "Klarte ikke å slette moduldata",
|
||||
"delete_module_button": "Slett",
|
||||
"delete_module_failed": "Klarte ikke å slette modulen",
|
||||
"documentation_button": "Docs",
|
||||
"documentation_button": "Dokumentasjon",
|
||||
"download_script_failed": "Klarte ikke å laste ned skript",
|
||||
"downloading_script": "Last ned skript...",
|
||||
"edit_module_button": "Rediger",
|
||||
@@ -389,12 +389,12 @@
|
||||
"scope_all": "Alle venner/grupper",
|
||||
"scope_whitelist": "Ingen andre enn",
|
||||
"scope_blacklist": "Alle bortsett fra",
|
||||
"events_section_title": "Events",
|
||||
"events_section_title": "Hendelser",
|
||||
"events_suffix": "hendelser",
|
||||
"no_events_text": "Ingen hendelser lagt til enda",
|
||||
"add_event_dialog_title": "Legg til hendelse",
|
||||
"event_type_label": "Event Type",
|
||||
"triggers_title": "Triggers",
|
||||
"event_type_label": "Hendelsestype",
|
||||
"triggers_title": "Utl?sere",
|
||||
"conditions_title": "Betingelser",
|
||||
"condition_only_inside_conversation": "Først når jeg er inne i samtalen",
|
||||
"condition_only_outside_conversation": "Først når jeg er utenfor samtale",
|
||||
@@ -546,7 +546,7 @@
|
||||
"name": "Bruk E2E-kryptering"
|
||||
},
|
||||
"pin_conversation": {
|
||||
"name": "Pin Conversation"
|
||||
"name": "Fest samtale"
|
||||
},
|
||||
"exclude_message_logger": {
|
||||
"name": "Utelat fra meldingslogger"
|
||||
@@ -595,7 +595,7 @@
|
||||
},
|
||||
"actions": {
|
||||
"clean_snapchat_cache": {
|
||||
"name": "Clean Snapchat Cache",
|
||||
"name": "Rens Snapchat-buffer",
|
||||
"description": "Rengjør Snapchat Cache"
|
||||
},
|
||||
"manage_friend_list": {
|
||||
@@ -645,7 +645,7 @@
|
||||
"app_appearance": {
|
||||
"always_light": "Alltid lys",
|
||||
"always_dark": "Alltid mørk",
|
||||
"null": "Match System"
|
||||
"null": "F?lg system"
|
||||
},
|
||||
"auto_reload": {
|
||||
"snapchat_only": "Last bare Snapchat på nytt",
|
||||
@@ -662,7 +662,7 @@
|
||||
"auto_download": "⬇️ Automatisk nedlasting",
|
||||
"auto_save": "💬 Lagre meldinger automatisk",
|
||||
"unsaveable_messages": "⬇️ Ubesparelige meldinger",
|
||||
"auto_open_snaps": "📷 Auto Open Snaps",
|
||||
"auto_open_snaps": "?? ?pne Snaps automatisk",
|
||||
"stealth": "👻 Stealth-modus",
|
||||
"auto_reply": "📨 Automatisk svar",
|
||||
"auto_delete_sent_messages": "🗑️ Auto Slett Sendte meldinger",
|
||||
@@ -716,7 +716,7 @@
|
||||
"notifications": {
|
||||
"chat_screenshot": "Skjermbilde",
|
||||
"chat_screen_record": "Skjermopptak",
|
||||
"snap_replay": "Snap Replay",
|
||||
"snap_replay": "Snap-avspilling",
|
||||
"camera_roll_save": "Kamerarulle Lagre",
|
||||
"chat": "Chat",
|
||||
"chat_reply": "Chat Svar",
|
||||
@@ -733,43 +733,43 @@
|
||||
"map_live_location": "Kart Live Beliggenhet"
|
||||
},
|
||||
"auto_read": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Svarteliste",
|
||||
"whitelist": "Hviteliste",
|
||||
"disabled": "Deaktivert"
|
||||
},
|
||||
"hide_typing_indicator": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Svarteliste",
|
||||
"whitelist": "Hviteliste",
|
||||
"disabled": "Deaktivert"
|
||||
},
|
||||
"auto_delete_sent_messages": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Svarteliste",
|
||||
"whitelist": "Hviteliste",
|
||||
"disabled": "Deaktivert"
|
||||
},
|
||||
"auto_download": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Svarteliste",
|
||||
"whitelist": "Hviteliste",
|
||||
"disabled": "Deaktivert"
|
||||
},
|
||||
"stealth": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Svarteliste",
|
||||
"whitelist": "Hviteliste",
|
||||
"disabled": "Deaktivert"
|
||||
},
|
||||
"auto_save": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Svarteliste",
|
||||
"whitelist": "Hviteliste",
|
||||
"disabled": "Deaktivert"
|
||||
},
|
||||
"message_logger": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Svarteliste",
|
||||
"whitelist": "Hviteliste",
|
||||
"disabled": "Deaktivert"
|
||||
},
|
||||
"auto_reply": {
|
||||
"blacklist": "Blacklist",
|
||||
"whitelist": "Whitelist",
|
||||
"blacklist": "Svarteliste",
|
||||
"whitelist": "Hviteliste",
|
||||
"disabled": "Deaktivert"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -888,17 +888,17 @@
|
||||
"null": "Automatisk"
|
||||
},
|
||||
"update_check_frequency": {
|
||||
"null": "Auto"
|
||||
"null": "Automatisk"
|
||||
},
|
||||
"snapchat_plus": {
|
||||
"not_subscribed": "Ikke Abonnert",
|
||||
"basic": "Grunnleggende",
|
||||
"ad_free": "Ad Free",
|
||||
"ad_free": "Reklamefri",
|
||||
"null": "Standard"
|
||||
},
|
||||
"bypass_video_length_restriction": {
|
||||
"single": "Enkeltmedier",
|
||||
"split": "Split media",
|
||||
"split": "Del opp media",
|
||||
"null": "Standard"
|
||||
},
|
||||
"old_bitmoji_selfie": {
|
||||
@@ -990,7 +990,7 @@
|
||||
"message_types": {
|
||||
"CHAT": "Chat",
|
||||
"SNAP": "Snap",
|
||||
"NOTE": "Note",
|
||||
"NOTE": "Notat",
|
||||
"EXTERNAL_MEDIA": "Eksterne medier",
|
||||
"STICKER": "Klistremerke"
|
||||
},
|
||||
@@ -1016,19 +1016,19 @@
|
||||
"friendly, casual, helpful, empathetic": "vennlig, casual, hjelpsom, empatisk"
|
||||
},
|
||||
"ai_response_style": {
|
||||
"casual": "Casual",
|
||||
"casual": "Uformell",
|
||||
"formal": "Formell",
|
||||
"friendly": "Vennlig",
|
||||
"humorous": "Humorøs",
|
||||
"empathetic": "Empatisk",
|
||||
"toxic": "Edgy",
|
||||
"toxic": "Kantete",
|
||||
"busy": "Opptatt"
|
||||
},
|
||||
"ai_temperature": {
|
||||
"0.7": "Balansert (0,7)"
|
||||
},
|
||||
"ai_response_language": {
|
||||
"auto": "Auto",
|
||||
"auto": "Automatisk",
|
||||
"en": "Norsk",
|
||||
"es": "Spansk",
|
||||
"fr": "Fransk",
|
||||
@@ -1085,12 +1085,12 @@
|
||||
"auto_reply_content_types": {
|
||||
"chat_messages": "Chatmeldinger",
|
||||
"snap_messages": "Snaps",
|
||||
"story_share_messages": "Story Shares",
|
||||
"story_share_messages": "Story-deling",
|
||||
"story_reply_messages": "Story Svar",
|
||||
"external_media_messages": "Eksterne medier",
|
||||
"voice_note_messages": "Stemmenotater",
|
||||
"sticker_messages": "Klistremerker",
|
||||
"tiny_snap_messages": "Tiny Snaps",
|
||||
"tiny_snap_messages": "Mini-snaps",
|
||||
"map_reaction_messages": "Kart Reaksjoner",
|
||||
"half_swipes": "Halvstrekk"
|
||||
},
|
||||
@@ -1139,7 +1139,7 @@
|
||||
"description": "Angi koordinatene for den spekulerte plasseringen"
|
||||
},
|
||||
"walk_radius": {
|
||||
"name": "Walk Radius",
|
||||
"name": "G?-radius",
|
||||
"description": "Tilfeldig gå rundt i denne radiusen (ft)"
|
||||
},
|
||||
"always_update_location": {
|
||||
@@ -1181,7 +1181,7 @@
|
||||
"description": "Slår av bildekomprimering når du laster opp medier"
|
||||
},
|
||||
"custom_image_upload_format": {
|
||||
"name": "Custom Image Upload Format",
|
||||
"name": "Tilpasset bildeopplastingsformat",
|
||||
"description": "Angi et egendefinert bildeopplastingsformat\nVelg et tapsfritt format (som PNG) for den beste kvaliteten"
|
||||
}
|
||||
}
|
||||
@@ -1191,7 +1191,7 @@
|
||||
"description": "Bekreft automatisk valgte handlinger"
|
||||
},
|
||||
"auto_updater": {
|
||||
"name": "Auto Updater",
|
||||
"name": "Auto-oppdaterer",
|
||||
"description": "Kontroller automatisk for nye oppdateringer"
|
||||
},
|
||||
"update_settings": {
|
||||
@@ -1210,7 +1210,7 @@
|
||||
"name": "UI-innstillinger",
|
||||
"properties": {
|
||||
"haptic_feedback": {
|
||||
"name": "Haptic Feedback"
|
||||
"name": "Haptisk tilbakemelding"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1281,7 +1281,7 @@
|
||||
}
|
||||
},
|
||||
"downloader": {
|
||||
"name": "Downloader",
|
||||
"name": "Nedlaster",
|
||||
"description": "Last ned Snapchat Media",
|
||||
"properties": {
|
||||
"save_folder": {
|
||||
@@ -1543,7 +1543,7 @@
|
||||
"description": "Hopper automatisk til neste Snap når det markeres en Snap som sett.\nBruk i kombinasjon med Mark Snap som sett knapp"
|
||||
},
|
||||
"loop_media_playback": {
|
||||
"name": "Loop Media Playback",
|
||||
"name": "Loop av medieavspilling",
|
||||
"description": "Loops media avspilling når du ser Snaps / Historier"
|
||||
},
|
||||
"disable_replay_in_ff": {
|
||||
@@ -1647,7 +1647,7 @@
|
||||
"description": "Forhindrer at dine egne meldinger slettes"
|
||||
},
|
||||
"auto_purge": {
|
||||
"name": "Auto Purge",
|
||||
"name": "Automatisk rensing",
|
||||
"description": "Sletter automatisk cachede meldinger som er eldre enn angitt tid"
|
||||
},
|
||||
"message_filter": {
|
||||
@@ -1713,7 +1713,7 @@
|
||||
}
|
||||
},
|
||||
"strip_media_metadata": {
|
||||
"name": "Strip Media Metadata",
|
||||
"name": "Fjern mediametadata",
|
||||
"description": "Fjerner metadata av medier før du sender som melding"
|
||||
},
|
||||
"bypass_message_retention_policy": {
|
||||
@@ -1729,7 +1729,7 @@
|
||||
"description": "Tillater deg å vise gruppeinformasjon etter å ha blitt sparket"
|
||||
},
|
||||
"double_tap_chat_action": {
|
||||
"name": "Double Tap Chat Action",
|
||||
"name": "Dobbelttrykk-chat-handling",
|
||||
"description": "Utfører en egendefinert handling når dobbelt trykk på en melding i chat"
|
||||
},
|
||||
"double_tap_chat_action_custom_emoji": {
|
||||
@@ -1781,7 +1781,7 @@
|
||||
"description": "Systemprompt som definerer AIs personlighet og oppførsel"
|
||||
},
|
||||
"ai_max_tokens": {
|
||||
"name": "AI Max Tokens",
|
||||
"name": "AI maks tokens",
|
||||
"description": "Maksimalt antall tokens (ord) AI kan bruke i svar"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1793,11 +1793,11 @@
|
||||
"description": "Antall tidligere meldinger som skal inkluderes som kontekst for AI-svar"
|
||||
},
|
||||
"ai_personality_traits": {
|
||||
"name": "AI Personality Traits",
|
||||
"name": "AI-personlighetstrekk",
|
||||
"description": "Kommaseparerte personlighetstrekk for AI (f.eks. vennlig, uformell, hjelpsom)"
|
||||
},
|
||||
"ai_response_style": {
|
||||
"name": "AI Response Style",
|
||||
"name": "AI-svarstil",
|
||||
"description": "Samlet stil for AI svar"
|
||||
},
|
||||
"ai_response_language": {
|
||||
@@ -1839,7 +1839,7 @@
|
||||
"description": "Hellig tekst å bruke når venn spesifikk hilsen er aktivert"
|
||||
},
|
||||
"auto_reply_content_types": {
|
||||
"name": "Auto Reply Triggers",
|
||||
"name": "Auto-svar-utl?sere",
|
||||
"description": "Velg hvilke meldingstyper som skal utløse automatisk"
|
||||
},
|
||||
"chat_messages": {
|
||||
@@ -1991,7 +1991,7 @@
|
||||
"description": "Paus oversettelse når tjenesten er blokkert"
|
||||
},
|
||||
"max_retries": {
|
||||
"name": "Max Retries",
|
||||
"name": "Maks antall fors?k",
|
||||
"description": "Maksimalt antall forsøk"
|
||||
},
|
||||
"retry_delay": {
|
||||
@@ -2068,7 +2068,7 @@
|
||||
"description": "Angi en egendefinert kameraoppløsning, bredde x høyde (f.eks. 1920x1080).\nTilpasset oppløsning må støttes av enheten"
|
||||
},
|
||||
"front_custom_frame_rate": {
|
||||
"name": "Front Custom Frame Rate",
|
||||
"name": "Tilpasset bildefrekvens foran",
|
||||
"description": "Overstyrer frontkamerarammehastigheten"
|
||||
},
|
||||
"back_custom_frame_rate": {
|
||||
@@ -2112,7 +2112,7 @@
|
||||
"description": "Eksperimentelle funksjoner",
|
||||
"properties": {
|
||||
"native_hooks": {
|
||||
"name": "Native Hooks",
|
||||
"name": "Native hooks",
|
||||
"description": "Usikre funksjoner som kroker inn i Snapchats opprinnelige kode",
|
||||
"properties": {
|
||||
"composer_hooks": {
|
||||
@@ -2180,7 +2180,7 @@
|
||||
"description": "Overstyr Android-ID-en sendt til Snapchat",
|
||||
"properties": {
|
||||
"spoof_android_id": {
|
||||
"name": "Spoof Android ID",
|
||||
"name": "Spoof Android-ID",
|
||||
"description": "Overstyr Android-ID-en som sendes til Snapchat med en egendefinert verdi"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -2204,15 +2204,15 @@
|
||||
"description": "Konverterer snaps til chat eksterne medier lokalt. Dette vises i chat nedlastings-menyen"
|
||||
},
|
||||
"media_file_picker": {
|
||||
"name": "Media File Picker",
|
||||
"name": "Mediefilvelger",
|
||||
"description": "La deg velge en video/audio-fil fra galleriet"
|
||||
},
|
||||
"story_logger": {
|
||||
"name": "Story Logger",
|
||||
"name": "Story-logg",
|
||||
"description": "Gir en historie av venner historier"
|
||||
},
|
||||
"call_recorder": {
|
||||
"name": "Call Recorder",
|
||||
"name": "Samtalsopptaker",
|
||||
"description": "Opptak av lydsamtaler automatisk"
|
||||
},
|
||||
"account_switcher": {
|
||||
@@ -2252,7 +2252,7 @@
|
||||
"description": "Lar deg legge til notater til venner profiler"
|
||||
},
|
||||
"cof_experiments": {
|
||||
"name": "COF Experiments",
|
||||
"name": "COF-eksperimenter",
|
||||
"description": "Aktiverer uutgitte/beta Snapchat-funksjoner"
|
||||
},
|
||||
"context_menu_fix": {
|
||||
@@ -2260,7 +2260,7 @@
|
||||
"description": "Prøv å reparere Venn Feed-menyen som når enheten er frakoblet kan den ikke vises riktig"
|
||||
},
|
||||
"app_lock": {
|
||||
"name": "App Lock",
|
||||
"name": "App-l?s",
|
||||
"description": "Forhindrer tilgang til Snapchat uten kode",
|
||||
"properties": {
|
||||
"lock_on_resume": {
|
||||
@@ -2270,7 +2270,7 @@
|
||||
}
|
||||
},
|
||||
"infinite_story_boost": {
|
||||
"name": "Infinite Story Boost",
|
||||
"name": "Uendelig Story-boost",
|
||||
"description": "Overgå historien Boost Limit forsinkelse"
|
||||
},
|
||||
"meo_passcode_bypass": {
|
||||
@@ -2322,7 +2322,7 @@
|
||||
}
|
||||
},
|
||||
"scripting": {
|
||||
"name": "Scripting",
|
||||
"name": "Skripting",
|
||||
"description": "Kjør egendefinerte skript for å utvide PurrfectSnap",
|
||||
"properties": {
|
||||
"developer_mode": {
|
||||
@@ -2364,7 +2364,7 @@
|
||||
"description": "La sporeren kjøre i bakgrunnen. Merk: Dette vil betydelig drenere batteriet ditt"
|
||||
},
|
||||
"auto_purge": {
|
||||
"name": "Auto Purge",
|
||||
"name": "Automatisk rensing",
|
||||
"description": "Sletter automatisk cachede hendelser som er eldre enn den angitte tiden"
|
||||
}
|
||||
}
|
||||
@@ -2402,29 +2402,29 @@
|
||||
"FAMILY_CENTER_ACCEPT": "Familiesenter aksepterer",
|
||||
"FAMILY_CENTER_LEAVE": "Familiesenter",
|
||||
"STATUS_PLUS_GIFT": "Status Plus-gave",
|
||||
"TINY_SNAP": "Tiny Snap",
|
||||
"TINY_SNAP": "Mini-snap",
|
||||
"STATUS_COUNTDOWN": "Nedtelling",
|
||||
"MAP_REACTION": "Kart Reaksjon",
|
||||
"chat_messages": "Chatmeldinger",
|
||||
"snap_messages": "Snaps",
|
||||
"story_share_messages": "Story Shares",
|
||||
"story_share_messages": "Story-deling",
|
||||
"story_reply_messages": "Story Svar",
|
||||
"external_media_messages": "Eksterne medier",
|
||||
"voice_note_messages": "Stemmenote",
|
||||
"sticker_messages": "Klistremerke",
|
||||
"tiny_snap_messages": "Tiny Snap",
|
||||
"tiny_snap_messages": "Mini-snap",
|
||||
"map_reaction_messages": "Kart Reaksjon",
|
||||
"half_swipes": "Halvstrekk"
|
||||
},
|
||||
"media_download_source": {
|
||||
"none": "Ingen",
|
||||
"pending": "Venter",
|
||||
"chat_media": "Chat Media",
|
||||
"chat_media": "Chat-medier",
|
||||
"story": "Story",
|
||||
"public_story": "Offentlig historie",
|
||||
"spotlight": "Spotlight",
|
||||
"profile_picture": "Profilbilde",
|
||||
"story_logger": "Story Logger",
|
||||
"story_logger": "Story-logg",
|
||||
"message_logger": "Meldingslogger",
|
||||
"merged": "Flettet",
|
||||
"voice_call": "Stemmesamtale"
|
||||
@@ -2553,7 +2553,7 @@
|
||||
"deleted": "Slettet",
|
||||
"business_accounts": "Forretningskontoer",
|
||||
"streaks": "Streaks",
|
||||
"non_streaks": "Non Streaks",
|
||||
"non_streaks": "Uten streaks",
|
||||
"followed": "Følgt",
|
||||
"following": "Følger",
|
||||
"location_on_map": "Beliggenhet på kart"
|
||||
@@ -2562,7 +2562,7 @@
|
||||
"none": "Ingen",
|
||||
"username": "Brukernavn",
|
||||
"added_timestamp": "Lagt til Timestamp",
|
||||
"snap_score": "Snap Score",
|
||||
"snap_score": "Snap-poeng",
|
||||
"streak_length": "Streak Lengde",
|
||||
"most_messages_sent": "De fleste meldinger sendt",
|
||||
"most_recent_message": "Siste melding",
|
||||
@@ -2619,10 +2619,10 @@
|
||||
"message_reaction_add": "Meldingsreaksjon Legg til",
|
||||
"message_reaction_remove": "Meldingsreaksjon Fjern",
|
||||
"snap_opened": "Snap åpnet",
|
||||
"snap_replayed": "Snap Replayed",
|
||||
"snap_replayed": "Snap spilt av igjen",
|
||||
"snap_replayed_twice": "Snap Replayed to ganger",
|
||||
"snap_screenshot": "Snap skjermbilde",
|
||||
"snap_screen_record": "Snap Screen Record",
|
||||
"snap_screen_record": "Snap-skjermopptak",
|
||||
"i_can_see_you": "Jeg kan se deg"
|
||||
},
|
||||
"cleared_from_feed": "Fjernet fra fôr",
|
||||
@@ -2660,8 +2660,8 @@
|
||||
"sticker": "Klistremerke",
|
||||
"gif": "GIF",
|
||||
"external_media": "Eksterne medier",
|
||||
"note": "Note",
|
||||
"original_story": "Original Story"
|
||||
"note": "Notat",
|
||||
"original_story": "Original-story"
|
||||
},
|
||||
"select_attachments_title": "Velg vedlegg",
|
||||
"download_started_toast": "Last ned startet",
|
||||
@@ -2721,7 +2721,7 @@
|
||||
},
|
||||
"auto_open_snaps": {
|
||||
"title": "Åpne snaps automatisk",
|
||||
"priority_title": "Auto Open Snaps (Priority)",
|
||||
"priority_title": "?pne Snaps automatisk (prioritet)",
|
||||
"error_title": "Åpne snaps automatisk (feil)",
|
||||
"channel_description": "Varsler for automatisk åpnende snaps kø status",
|
||||
"priority_channel_description": "Høy prioritetsvarsler for auto-åpning snaps",
|
||||
@@ -2759,7 +2759,7 @@
|
||||
"notification_statistics": "STATISTIK",
|
||||
"notification_queue_size": "Køstørrelse",
|
||||
"notification_total_opened": "Total Snaps åpnet",
|
||||
"notification_queue_preview": "QUEUE PREVIEW",
|
||||
"notification_queue_preview": "K?FORH?NDSVISNING",
|
||||
"notification_processing_continue": "Behandling vil fortsette automatisk...",
|
||||
"notification_no_snaps_queue": "Ingen snaps i kø.",
|
||||
"notification_queue_cleared_opened": "Køyeklart ({opened}åpnet)",
|
||||
@@ -2948,7 +2948,7 @@
|
||||
"types": {
|
||||
"username": "Brukernavn",
|
||||
"conversation": "Kontakt",
|
||||
"event": "Event"
|
||||
"event": "Hendelse"
|
||||
},
|
||||
"event_types": {
|
||||
"conversation_enter": "Entert samtale",
|
||||
@@ -2967,7 +2967,7 @@
|
||||
"message_reaction_add": "Legg til reaksjon",
|
||||
"message_reaction_remove": "Fjernet reaksjon",
|
||||
"snap_opened": "Åpnet snap",
|
||||
"snap_replayed": "Replayed snap",
|
||||
"snap_replayed": "Spilte av snap igjen",
|
||||
"snap_replayed_twice": "Replayed snap to ganger",
|
||||
"snap_screenshot": "Tatt skjermbilde",
|
||||
"snap_screen_record": "Skjermen registrert"
|
||||
@@ -2993,7 +2993,7 @@
|
||||
"in_conversation": "i{conversation}",
|
||||
"unknown_user": "Ukjend",
|
||||
"unknown_conversation": "DMs",
|
||||
"i_can_see_you_entered": "Entered",
|
||||
"i_can_see_you_entered": "Gikk inn",
|
||||
"i_can_see_you_left": "Venstre",
|
||||
"i_can_see_you_duration": "Varighet",
|
||||
"i_can_see_you_not_available": "N/A",
|
||||
@@ -3018,7 +3018,7 @@
|
||||
"message_reaction_add": "tilsatt en reaksjon",
|
||||
"message_reaction_remove": "fjernet en reaksjon",
|
||||
"snap_opened": "åpnet en snap",
|
||||
"snap_replayed": "replayed a snap",
|
||||
"snap_replayed": "spilte av en snap igjen",
|
||||
"snap_replayed_twice": "replayed a snap to ganger",
|
||||
"snap_screenshot": "ta et skjermbilde",
|
||||
"snap_screen_record": "skjerm innspilt",
|
||||
@@ -3029,10 +3029,10 @@
|
||||
"edit_rule": {
|
||||
"custom_rule": "Egendefinert regel",
|
||||
"scope": "Område",
|
||||
"events": "Events",
|
||||
"events": "Hendelser",
|
||||
"add_event": "Legg til hendelse",
|
||||
"type": "Type",
|
||||
"triggers": "Triggers",
|
||||
"triggers": "Utl?sere",
|
||||
"conditions": "Betingelser",
|
||||
"only_inside_conversation": "Først når jeg er inne i samtalen",
|
||||
"only_outside_conversation": "Først når jeg er utenfor samtale",
|
||||
@@ -3064,13 +3064,13 @@
|
||||
}
|
||||
},
|
||||
"ui_settings_title": "UI-innstillinger",
|
||||
"haptic_feedback_label": "Haptic Feedback",
|
||||
"haptic_feedback_label": "Haptisk tilbakemelding",
|
||||
"updates_title": "Oppdateringer",
|
||||
"auto_update_check": "Automatisk oppdateringskontroll",
|
||||
"update_check_frequency_daily": "Daglig",
|
||||
"update_check_frequency_weekly": "Uken",
|
||||
"update_check_frequency_monthly": "Månedlig",
|
||||
"update_channel_stable": "Stable",
|
||||
"update_channel_stable": "Stabil",
|
||||
"update_channel_prerelease": "Forutsetning",
|
||||
"friend_notes_title": "Vennnotater",
|
||||
"friend_notes_description": "Administrere og sikkerhetskopiere dine vennenotater",
|
||||
@@ -3182,7 +3182,7 @@
|
||||
"failed_to_edit_message": "Klarte ikke å redigere melding:{error}"
|
||||
},
|
||||
"ai_response_style": {
|
||||
"casual": "Casual",
|
||||
"casual": "Uformell",
|
||||
"formal": "Formell",
|
||||
"friendly": "Vennlig",
|
||||
"humorous": "Humorøs",
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Taal selecteren",
|
||||
@@ -30,7 +30,7 @@
|
||||
"home": "Begin",
|
||||
"home_about": "Info",
|
||||
"home_settings": "Instellingen",
|
||||
"home_logs": "Logs",
|
||||
"home_logs": "Logboeken",
|
||||
"logger_history": "Loggergeschiedenis",
|
||||
"logged_stories": "Gelogde verhalen",
|
||||
"friend_tracker": "Vriend Tracker",
|
||||
@@ -52,13 +52,13 @@
|
||||
"customize_bottom_bar_subtitle": "Kies welke tabbladen getoond worden op uw startscherm",
|
||||
"available_tabs_title": "Beschikbare tabbladen",
|
||||
"shown_tabs_title": "Tabbladen tonen",
|
||||
"reset_button": "Reset",
|
||||
"reset_button": "Resetten",
|
||||
"done_button": "Klaar"
|
||||
},
|
||||
"sections": {
|
||||
"home": {
|
||||
"version_title": "v{versionName}· door Eternal",
|
||||
"update_title": "PurrfectSnap Update",
|
||||
"update_title": "PurrfectSnap-update",
|
||||
"update_content": "Versie{version}is beschikbaar!",
|
||||
"update_button": "Downloaden",
|
||||
"debug_build_summary_title": "U draait een debug bouw van PurrfectSnap",
|
||||
@@ -92,7 +92,7 @@
|
||||
"update_check_frequency_weekly": "Wekelijks",
|
||||
"update_check_frequency_monthly": "Maandelijks",
|
||||
"update_channel_stable": "Stabiel",
|
||||
"update_channel_prerelease": "Pre-release",
|
||||
"update_channel_prerelease": "Vooruitgave",
|
||||
"update_notification_channel_name": "Bijwerken",
|
||||
"update_notification_channel_description": "Waarschuwen wanneer nieuwe releases beschikbaar zijn",
|
||||
"update_notification_title": "Nieuwe update beschikbaar",
|
||||
@@ -113,7 +113,7 @@
|
||||
"customize_bottom_bar_title": "Onderbalk aanpassen",
|
||||
"customize_bottom_bar_subtitle": "Kies welke tabbladen getoond worden op uw startscherm",
|
||||
"available_tabs_title": "Beschikbare tabbladen",
|
||||
"reset_button": "Reset",
|
||||
"reset_button": "Resetten",
|
||||
"done_button": "Klaar",
|
||||
"clear_friend_feed": "Vriend Feed wissen",
|
||||
"test_mode_label": "PurrAura inschakelen",
|
||||
@@ -136,7 +136,7 @@
|
||||
"disabled": "Uitgeschakeld",
|
||||
"export_option": "Uitvoer",
|
||||
"import_option": "Importeren",
|
||||
"reset_option": "Reset",
|
||||
"reset_option": "Resetten",
|
||||
"config_export_success_toast": "Configureren met succes",
|
||||
"config_import_success_toast": "Configureren met succes",
|
||||
"config_import_failure_toast": "Kon config niet importeren{error}",
|
||||
@@ -360,7 +360,7 @@
|
||||
},
|
||||
"friend_tracker": {
|
||||
"rules_tab": "Regels",
|
||||
"logs_tab": "Logs",
|
||||
"logs_tab": "Logboeken",
|
||||
"catalog_button": "Catalogus",
|
||||
"add_rule_button": "Regel toevoegen",
|
||||
"import_button": "Importeren",
|
||||
@@ -711,7 +711,7 @@
|
||||
"started": "Gestart",
|
||||
"success": "Succes",
|
||||
"progress": "Voortgang",
|
||||
"failure": "Failure"
|
||||
"failure": "Mislukking"
|
||||
},
|
||||
"notifications": {
|
||||
"chat_screenshot": "Schermafdruk",
|
||||
@@ -734,42 +734,42 @@
|
||||
},
|
||||
"auto_read": {
|
||||
"blacklist": "Zwarte lijst",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Witte lijst",
|
||||
"disabled": "Uitgeschakeld"
|
||||
},
|
||||
"hide_typing_indicator": {
|
||||
"blacklist": "Zwarte lijst",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Witte lijst",
|
||||
"disabled": "Uitgeschakeld"
|
||||
},
|
||||
"auto_delete_sent_messages": {
|
||||
"blacklist": "Zwarte lijst",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Witte lijst",
|
||||
"disabled": "Uitgeschakeld"
|
||||
},
|
||||
"auto_download": {
|
||||
"blacklist": "Zwarte lijst",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Witte lijst",
|
||||
"disabled": "Uitgeschakeld"
|
||||
},
|
||||
"stealth": {
|
||||
"blacklist": "Zwarte lijst",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Witte lijst",
|
||||
"disabled": "Uitgeschakeld"
|
||||
},
|
||||
"auto_save": {
|
||||
"blacklist": "Zwarte lijst",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Witte lijst",
|
||||
"disabled": "Uitgeschakeld"
|
||||
},
|
||||
"message_logger": {
|
||||
"blacklist": "Zwarte lijst",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Witte lijst",
|
||||
"disabled": "Uitgeschakeld"
|
||||
},
|
||||
"auto_reply": {
|
||||
"blacklist": "Zwarte lijst",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Witte lijst",
|
||||
"disabled": "Uitgeschakeld"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -888,7 +888,7 @@
|
||||
"null": "Automatisch"
|
||||
},
|
||||
"update_check_frequency": {
|
||||
"null": "Auto"
|
||||
"null": "Automatisch"
|
||||
},
|
||||
"snapchat_plus": {
|
||||
"not_subscribed": "Niet ingeschreven",
|
||||
@@ -1016,7 +1016,7 @@
|
||||
"friendly, casual, helpful, empathetic": "vriendelijk, casual, behulpzaam, empathisch"
|
||||
},
|
||||
"ai_response_style": {
|
||||
"casual": "Casual",
|
||||
"casual": "Informeel",
|
||||
"formal": "Formele",
|
||||
"friendly": "Vriendelijk",
|
||||
"humorous": "Humoristisch",
|
||||
@@ -1028,7 +1028,7 @@
|
||||
"0.7": "Gebalanceerd (0,7)"
|
||||
},
|
||||
"ai_response_language": {
|
||||
"auto": "Auto",
|
||||
"auto": "Automatisch",
|
||||
"en": "Engels",
|
||||
"es": "Spaans",
|
||||
"fr": "Frans",
|
||||
@@ -1050,7 +1050,7 @@
|
||||
"fi": "Fins"
|
||||
},
|
||||
"friendGreeting": {
|
||||
"Hey": "Hey"
|
||||
"Hey": "Hoi"
|
||||
},
|
||||
"half_swipe_messages": {
|
||||
"[\"I noticed you half-swiped! I'll respond soon.\"]": "Ik zag dat je halfgeknipt was. Ik zal snel reageren."
|
||||
@@ -1445,7 +1445,7 @@
|
||||
"description": "Selecteer welke UI-componenten te verbergen zijn"
|
||||
},
|
||||
"opera_media_quick_info": {
|
||||
"name": "Opera Media Quick Info",
|
||||
"name": "Opera Media snelinfo",
|
||||
"description": "Toont nuttige informatie van media zoals aanmaakdatum in het contextmenu van de operaviewer"
|
||||
},
|
||||
"old_bitmoji_selfie": {
|
||||
@@ -1713,7 +1713,7 @@
|
||||
}
|
||||
},
|
||||
"strip_media_metadata": {
|
||||
"name": "Strip Media Metadata",
|
||||
"name": "Metadata van media verwijderen",
|
||||
"description": "Verwijdert metadata van media voordat u als bericht verstuurt"
|
||||
},
|
||||
"bypass_message_retention_policy": {
|
||||
@@ -1761,7 +1761,7 @@
|
||||
"description": "Gebruik AI om intelligente automatische antwoorden te genereren in plaats van sjabloonberichten"
|
||||
},
|
||||
"ai_provider": {
|
||||
"name": "AI Provider",
|
||||
"name": "AI-provider",
|
||||
"description": "Selecteer welke AI-service moet worden gebruikt voor het genereren van antwoorden"
|
||||
},
|
||||
"ai_endpoint_url": {
|
||||
@@ -1781,7 +1781,7 @@
|
||||
"description": "Systeemprompt die de persoonlijkheid en het gedrag van de AI definieert"
|
||||
},
|
||||
"ai_max_tokens": {
|
||||
"name": "AI Max Tokens",
|
||||
"name": "AI max tokens",
|
||||
"description": "Maximum aantal tokens (woorden) dat de AI kan gebruiken in antwoorden"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1991,7 +1991,7 @@
|
||||
"description": "Vertaling pauzeren bij geblokkeerde dienst"
|
||||
},
|
||||
"max_retries": {
|
||||
"name": "Max Retries",
|
||||
"name": "Max. herhalingen",
|
||||
"description": "Maximum aantal herhalingspogingen"
|
||||
},
|
||||
"retry_delay": {
|
||||
@@ -2112,7 +2112,7 @@
|
||||
"description": "Experimentele kenmerken",
|
||||
"properties": {
|
||||
"native_hooks": {
|
||||
"name": "Native Hooks",
|
||||
"name": "Native hooks",
|
||||
"description": "Onveilige functies die Snapchat's native code haaken",
|
||||
"properties": {
|
||||
"composer_hooks": {
|
||||
@@ -2124,7 +2124,7 @@
|
||||
"description": "Toont de eerste aangemaakte gebruikersnaam naast de huidige gebruikersnaam in de profielpagina"
|
||||
},
|
||||
"bypass_camera_roll_limit": {
|
||||
"name": "Bypass Camera Roll Limit",
|
||||
"name": "Camerarol-limiet omzeilen",
|
||||
"description": "Verhoogt de maximale hoeveelheid media die u vanaf de camerarol kunt verzenden"
|
||||
},
|
||||
"custom_self_destruct_snap_delay": {
|
||||
@@ -2180,7 +2180,7 @@
|
||||
"description": "Android-ID naar Snapchat negeren",
|
||||
"properties": {
|
||||
"spoof_android_id": {
|
||||
"name": "Spoof Android ID",
|
||||
"name": "Android-ID spoofen",
|
||||
"description": "De Android-ID die naar Snapchat wordt verzonden met een aangepaste waarde negeren"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -2208,7 +2208,7 @@
|
||||
"description": "Hiermee kunt u een video/audiobestand uit de galerij te kiezen"
|
||||
},
|
||||
"story_logger": {
|
||||
"name": "Story Logger",
|
||||
"name": "Story-logger",
|
||||
"description": "Biedt een geschiedenis van vrienden verhalen"
|
||||
},
|
||||
"call_recorder": {
|
||||
@@ -2238,7 +2238,7 @@
|
||||
"description": "De voorkeurstaal voor het voicenote transcript (bv. EN, ES, FR)"
|
||||
},
|
||||
"notification_transcript": {
|
||||
"name": "Notification Transcript",
|
||||
"name": "Notificatietranscript",
|
||||
"description": "Voice notes overschrijven in meldingen\nDeze functie vereist dat de Chat Preview functie wordt ingeschakeld in betere meldingen"
|
||||
}
|
||||
}
|
||||
@@ -2322,7 +2322,7 @@
|
||||
}
|
||||
},
|
||||
"scripting": {
|
||||
"name": "Scripting",
|
||||
"name": "Scripten",
|
||||
"description": "Aangepaste scripts uitvoeren om PurrfectSnap te verlengen",
|
||||
"properties": {
|
||||
"developer_mode": {
|
||||
@@ -2401,7 +2401,7 @@
|
||||
"FAMILY_CENTER_INVITE": "Uitnodiging familiecentrum",
|
||||
"FAMILY_CENTER_ACCEPT": "Familiecentrum accepteren",
|
||||
"FAMILY_CENTER_LEAVE": "Familiecentrumverlof",
|
||||
"STATUS_PLUS_GIFT": "Status Plus Gift",
|
||||
"STATUS_PLUS_GIFT": "Status Plus-cadeau",
|
||||
"TINY_SNAP": "Kleine snap",
|
||||
"STATUS_COUNTDOWN": "Aftellen",
|
||||
"MAP_REACTION": "Kaartreactie",
|
||||
@@ -2410,7 +2410,7 @@
|
||||
"story_share_messages": "Story-aandelen",
|
||||
"story_reply_messages": "Verhaal Antwoorden",
|
||||
"external_media_messages": "Externe media",
|
||||
"voice_note_messages": "Voice Note",
|
||||
"voice_note_messages": "Spraakbericht",
|
||||
"sticker_messages": "Sticker",
|
||||
"tiny_snap_messages": "Kleine snap",
|
||||
"map_reaction_messages": "Kaartreactie",
|
||||
@@ -2424,7 +2424,7 @@
|
||||
"public_story": "Publiek verhaal",
|
||||
"spotlight": "Spotlight",
|
||||
"profile_picture": "Profielafbeelding",
|
||||
"story_logger": "Story Logger",
|
||||
"story_logger": "Story-logger",
|
||||
"message_logger": "Berichtlogger",
|
||||
"merged": "Samengevoegd",
|
||||
"voice_call": "Stemgesprek"
|
||||
@@ -2596,7 +2596,7 @@
|
||||
"negative": "Nee",
|
||||
"cancel": "Annuleren",
|
||||
"save": "Opslaan",
|
||||
"open": "Open",
|
||||
"open": "Openen",
|
||||
"download": "Downloaden",
|
||||
"send": "Verzenden",
|
||||
"restore_original": "Origineel herstellen",
|
||||
@@ -2627,7 +2627,7 @@
|
||||
},
|
||||
"cleared_from_feed": "Ontruimd van voer",
|
||||
"tracker_actions": {
|
||||
"log": "Log",
|
||||
"log": "Loggen",
|
||||
"in_app_notification": "In-App kennisgeving",
|
||||
"push_notification": "Aanmelding pushen",
|
||||
"custom": "Aangepast"
|
||||
@@ -2679,7 +2679,7 @@
|
||||
"failed_gallery_toast": "Opslaan in galerij is mislukt{error}",
|
||||
"dash_no_chapter": "Geen hoofdstuk gevonden",
|
||||
"dash_dialog": {
|
||||
"title": "Download dash media",
|
||||
"title": "Dash-media downloaden",
|
||||
"download_all": "Alles downloaden",
|
||||
"segment_text": "Segment{from}-{to}"
|
||||
}
|
||||
@@ -2759,7 +2759,7 @@
|
||||
"notification_statistics": "STATISTIEK",
|
||||
"notification_queue_size": "Wachtrijgrootte",
|
||||
"notification_total_opened": "Totaal aantal geopende snaps",
|
||||
"notification_queue_preview": "QUEUE PREVIEW",
|
||||
"notification_queue_preview": "WACHTRIJ-VOORBEELD",
|
||||
"notification_processing_continue": "De verwerking gaat automatisch door...",
|
||||
"notification_no_snaps_queue": "Geen kiekjes in de rij.",
|
||||
"notification_queue_cleared_opened": "Wachtrij is leeg ({opened}geopend)",
|
||||
@@ -2921,7 +2921,7 @@
|
||||
},
|
||||
"tracker": {
|
||||
"tabs": {
|
||||
"logs": "Logs",
|
||||
"logs": "Logboeken",
|
||||
"rules": "Regels"
|
||||
},
|
||||
"actions": {
|
||||
@@ -3071,7 +3071,7 @@
|
||||
"update_check_frequency_weekly": "Wekelijks",
|
||||
"update_check_frequency_monthly": "Maandelijks",
|
||||
"update_channel_stable": "Stabiel",
|
||||
"update_channel_prerelease": "Pre-release",
|
||||
"update_channel_prerelease": "Vooruitgave",
|
||||
"friend_notes_title": "Vriendelijke opmerkingen",
|
||||
"friend_notes_description": "Beheer en backup van uw vriend notities",
|
||||
"app_theme_title": "App-thema",
|
||||
@@ -3170,7 +3170,7 @@
|
||||
},
|
||||
"debug_dialogs": {
|
||||
"info": "Informatie",
|
||||
"refs": "Refs",
|
||||
"refs": "Referenties",
|
||||
"arroyo": "Arroyo",
|
||||
"message": "Bericht",
|
||||
"media_references": "Media Referenties",
|
||||
@@ -3182,7 +3182,7 @@
|
||||
"failed_to_edit_message": "Bewerken van bericht mislukt:{error}"
|
||||
},
|
||||
"ai_response_style": {
|
||||
"casual": "Casual",
|
||||
"casual": "Informeel",
|
||||
"formal": "Formele",
|
||||
"friendly": "Vriendelijk",
|
||||
"humorous": "Humoristisch",
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Wybierz język",
|
||||
@@ -52,7 +52,7 @@
|
||||
"customize_bottom_bar_subtitle": "Wybierz, które karty pokazać na ekranie",
|
||||
"available_tabs_title": "Dostępne karty",
|
||||
"shown_tabs_title": "Pokazane karty",
|
||||
"reset_button": "Reset",
|
||||
"reset_button": "Resetuj",
|
||||
"done_button": "Gotowe"
|
||||
},
|
||||
"sections": {
|
||||
@@ -84,7 +84,7 @@
|
||||
"clear_button": "Czysto",
|
||||
"view_logger_history_button": "Widok Historia logowania",
|
||||
"ui_settings_title": "Ustawienia interfejsu użytkownika",
|
||||
"haptic_feedback_label": "Haptic Feedback",
|
||||
"haptic_feedback_label": "Haptyczne sprz??enie zwrotne",
|
||||
"use_system_toasts_label": "Użyj tostów systemowych",
|
||||
"updates_title": "Aktualizacje",
|
||||
"auto_update_check": "Automatyczne sprawdzanie aktualizacji",
|
||||
@@ -113,9 +113,9 @@
|
||||
"customize_bottom_bar_title": "Dostosuj pasek dolny",
|
||||
"customize_bottom_bar_subtitle": "Wybierz, które karty pokazać na ekranie",
|
||||
"available_tabs_title": "Dostępne karty",
|
||||
"reset_button": "Reset",
|
||||
"reset_button": "Resetuj",
|
||||
"done_button": "Gotowe",
|
||||
"clear_friend_feed": "Clear Friend Feed",
|
||||
"clear_friend_feed": "Wyczy?? kana? znajomych",
|
||||
"test_mode_label": "Włącz PurrAura",
|
||||
"disable_feature_loading_label": "Wyłącz ładowanie funkcji",
|
||||
"disable_auto_mapper_label": "Wyłącz automapper",
|
||||
@@ -135,8 +135,8 @@
|
||||
"features": {
|
||||
"disabled": "Wyłączone",
|
||||
"export_option": "Eksport",
|
||||
"import_option": "Import",
|
||||
"reset_option": "Reset",
|
||||
"import_option": "Importuj",
|
||||
"reset_option": "Resetuj",
|
||||
"config_export_success_toast": "Konfiguracja eksportowana pomyślnie",
|
||||
"config_import_success_toast": "Konfiguracja importowana pomyślnie",
|
||||
"config_import_failure_toast": "Nie udało się zaimportować pliku konfiguracyjnego{error}",
|
||||
@@ -310,7 +310,7 @@
|
||||
"downloading_script": "Pobieranie skryptu...",
|
||||
"edit_module_button": "Edycja",
|
||||
"enter_url_label": "Wprowadź URL",
|
||||
"import_button": "Import",
|
||||
"import_button": "Importuj",
|
||||
"import_from_url_button": "Importuj z URL",
|
||||
"import_script_from_url_title": "Importuj skrypt z URL",
|
||||
"import_script_warning": "Zainstaluj skrypty tylko ze źródeł, którym ufasz.",
|
||||
@@ -363,10 +363,10 @@
|
||||
"logs_tab": "Logi",
|
||||
"catalog_button": "Katalog",
|
||||
"add_rule_button": "Dodaj zasadę",
|
||||
"import_button": "Import",
|
||||
"import_button": "Importuj",
|
||||
"filters_title": "Filtry",
|
||||
"search_by_label": "Szukaj",
|
||||
"newest_first_label": "Newest first",
|
||||
"newest_first_label": "Najnowsze najpierw",
|
||||
"since_label": "Od",
|
||||
"until_label": "Do",
|
||||
"unit_label": "Jednostka",
|
||||
@@ -438,7 +438,7 @@
|
||||
},
|
||||
"friend_tracker_import": {
|
||||
"title": "Importuj śledzenie znajomych",
|
||||
"confirm_button": "Import",
|
||||
"confirm_button": "Importuj",
|
||||
"back_button_description": "Wracaj",
|
||||
"expand_button_description": "Kategoria rozszerzania lub załamania",
|
||||
"imported_toast": "Tracker importowany",
|
||||
@@ -479,7 +479,7 @@
|
||||
"config_import": {
|
||||
"title": "Importuj podsumowanie konfiguracji",
|
||||
"back_button_description": "Wracaj",
|
||||
"confirm_button": "Import",
|
||||
"confirm_button": "Importuj",
|
||||
"expand_button_description": "Kategoria rozszerzania lub załamania",
|
||||
"enabled": "Włączone",
|
||||
"disabled": "Wyłączone",
|
||||
@@ -615,7 +615,7 @@
|
||||
"description": "Wykonuje operacje takie jak usuwanie przyjaciół lub masowe usuwanie rozmów"
|
||||
},
|
||||
"regen_mappings": {
|
||||
"name": "Regenerate Mappings",
|
||||
"name": "Regeneruj mapowania",
|
||||
"description": "Mappingi regenerowane ręcznie"
|
||||
},
|
||||
"change_language": {
|
||||
@@ -734,42 +734,42 @@
|
||||
},
|
||||
"auto_read": {
|
||||
"blacklist": "Czarna lista",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista dozwolonych",
|
||||
"disabled": "Wyłączone"
|
||||
},
|
||||
"hide_typing_indicator": {
|
||||
"blacklist": "Czarna lista",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista dozwolonych",
|
||||
"disabled": "Wyłączone"
|
||||
},
|
||||
"auto_delete_sent_messages": {
|
||||
"blacklist": "Czarna lista",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista dozwolonych",
|
||||
"disabled": "Wyłączone"
|
||||
},
|
||||
"auto_download": {
|
||||
"blacklist": "Czarna lista",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista dozwolonych",
|
||||
"disabled": "Wyłączone"
|
||||
},
|
||||
"stealth": {
|
||||
"blacklist": "Czarna lista",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista dozwolonych",
|
||||
"disabled": "Wyłączone"
|
||||
},
|
||||
"auto_save": {
|
||||
"blacklist": "Czarna lista",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista dozwolonych",
|
||||
"disabled": "Wyłączone"
|
||||
},
|
||||
"message_logger": {
|
||||
"blacklist": "Czarna lista",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista dozwolonych",
|
||||
"disabled": "Wyłączone"
|
||||
},
|
||||
"auto_reply": {
|
||||
"blacklist": "Czarna lista",
|
||||
"whitelist": "Whitelist",
|
||||
"whitelist": "Lista dozwolonych",
|
||||
"disabled": "Wyłączone"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -888,12 +888,12 @@
|
||||
"null": "Automatyczne"
|
||||
},
|
||||
"update_check_frequency": {
|
||||
"null": "Auto"
|
||||
"null": "Automatycznie"
|
||||
},
|
||||
"snapchat_plus": {
|
||||
"not_subscribed": "Nie subskrybowane",
|
||||
"basic": "Podstawowe",
|
||||
"ad_free": "Ad Free",
|
||||
"ad_free": "Bez reklam",
|
||||
"null": "Domyślne"
|
||||
},
|
||||
"bypass_video_length_restriction": {
|
||||
@@ -909,7 +909,7 @@
|
||||
"disable_confirmation_dialogs": {
|
||||
"erase_message": "Usuń wiadomość",
|
||||
"remove_friend": "Usuń przyjaciela",
|
||||
"block_friend": "Block Friend",
|
||||
"block_friend": "Zablokuj znajomego",
|
||||
"ignore_friend": "Ignoruj Przyjaciela",
|
||||
"hide_friend": "Ukryj przyjaciela",
|
||||
"hide_conversation": "Ukryj rozmowę",
|
||||
@@ -1021,14 +1021,14 @@
|
||||
"friendly": "Przyjazny",
|
||||
"humorous": "Zabawny",
|
||||
"empathetic": "Empatia",
|
||||
"toxic": "Edgy",
|
||||
"toxic": "Ostry",
|
||||
"busy": "Zajęty"
|
||||
},
|
||||
"ai_temperature": {
|
||||
"0.7": "Zbalansowany (0,7)"
|
||||
},
|
||||
"ai_response_language": {
|
||||
"auto": "Auto",
|
||||
"auto": "Automatycznie",
|
||||
"en": "Angielski",
|
||||
"es": "Hiszpański",
|
||||
"fr": "Francuski",
|
||||
@@ -1092,7 +1092,7 @@
|
||||
"sticker_messages": "Naklejki",
|
||||
"tiny_snap_messages": "Małe trzaski",
|
||||
"map_reaction_messages": "Reakcje mapy",
|
||||
"half_swipes": "Half Swipes"
|
||||
"half_swipes": "Po?owiczne przesuni?cia"
|
||||
},
|
||||
"supported_languages": {
|
||||
"en": "Angielski",
|
||||
@@ -1112,7 +1112,7 @@
|
||||
"translation_position": {
|
||||
"above": "Powyższy tekst",
|
||||
"below": "Poniżej tekstu",
|
||||
"inline": "Inline"
|
||||
"inline": "W linii"
|
||||
},
|
||||
"source_language": {
|
||||
"auto": "Wykrywanie automatycznie"
|
||||
@@ -1139,7 +1139,7 @@
|
||||
"description": "Ustaw współrzędne lokalizacji"
|
||||
},
|
||||
"walk_radius": {
|
||||
"name": "Walk Radius",
|
||||
"name": "Promie? chodzenia",
|
||||
"description": "Losowo chodzić w tym promieniu (ft)"
|
||||
},
|
||||
"always_update_location": {
|
||||
@@ -1191,7 +1191,7 @@
|
||||
"description": "Automatycznie potwierdza wybrane działania"
|
||||
},
|
||||
"auto_updater": {
|
||||
"name": "Auto Updater",
|
||||
"name": "Auto-aktualizator",
|
||||
"description": "Automatyczne sprawdzanie nowych aktualizacji"
|
||||
},
|
||||
"update_settings": {
|
||||
@@ -1210,7 +1210,7 @@
|
||||
"name": "Ustawienia interfejsu użytkownika",
|
||||
"properties": {
|
||||
"haptic_feedback": {
|
||||
"name": "Haptic Feedback"
|
||||
"name": "Haptyczne sprz??enie zwrotne"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1341,7 +1341,7 @@
|
||||
"description": "Ilość wątków do użycia"
|
||||
},
|
||||
"preset": {
|
||||
"name": "Preset",
|
||||
"name": "Ustawienie wst?pne",
|
||||
"description": "Ustaw prędkość konwersji"
|
||||
},
|
||||
"constant_rate_factor": {
|
||||
@@ -1399,7 +1399,7 @@
|
||||
"description": "Wyświetla mały podgląd obok niewidocznych Zatrzasków w czacie"
|
||||
},
|
||||
"bootstrap_override": {
|
||||
"name": "Bootstrap Override",
|
||||
"name": "Nadpisanie bootstrap",
|
||||
"description": "Przekroczenie ustawień interfejsu użytkownika bootstrap",
|
||||
"properties": {
|
||||
"app_appearance": {
|
||||
@@ -1449,7 +1449,7 @@
|
||||
"description": "Pokazuje użyteczne informacje o mediach, takie jak data utworzenia w menu kontekstowym przeglądarki operowej"
|
||||
},
|
||||
"old_bitmoji_selfie": {
|
||||
"name": "Old Bitmoji Selfie",
|
||||
"name": "Stare selfie Bitmoji",
|
||||
"description": "Przynosi Bitmoji selfie ze starszych wersji snapchat"
|
||||
},
|
||||
"disable_spotlight": {
|
||||
@@ -1465,7 +1465,7 @@
|
||||
"description": "Automatycznie zamyka menu Friend Feed po naciśnięciu przycisku nastawienia"
|
||||
},
|
||||
"vertical_story_viewer": {
|
||||
"name": "Vertical Story Viewer",
|
||||
"name": "Pionowy podgl?d Story",
|
||||
"description": "Włącza przeglądarkę pionową dla wszystkich historii"
|
||||
},
|
||||
"enable_friend_feed_menu_bar": {
|
||||
@@ -1535,11 +1535,11 @@
|
||||
"description": "Automatycznie oznacza wiadomości / zatrzaski jako przeczytane nawet wtedy, gdy włączony jest tryb stealth"
|
||||
},
|
||||
"mark_snap_as_seen_button": {
|
||||
"name": "Mark Snap as Seen Button",
|
||||
"name": "Przycisk oznaczania Snap jako obejrzany",
|
||||
"description": "Dodaje przycisk, aby zaznaczyć Snap, jak widać podczas oglądania.\nBędzie to działać nawet wtedy, gdy tryb stealth jest włączony"
|
||||
},
|
||||
"skip_when_marking_as_seen": {
|
||||
"name": "Skip When Marking as Seen",
|
||||
"name": "Pomijaj przy oznaczaniu jako obejrzane",
|
||||
"description": "Automatycznie przeskakuje do następnego Snap przy oznaczaniu Snap jak widać.\nUżyj w połączeniu z Mark Snap jako Seen Button"
|
||||
},
|
||||
"loop_media_playback": {
|
||||
@@ -1674,7 +1674,7 @@
|
||||
},
|
||||
"snap": {
|
||||
"name": "Pstryknięcia",
|
||||
"description": "Make snaps unsaveable"
|
||||
"description": "Uczy? Snaps niezapisywalne"
|
||||
},
|
||||
"external_media": {
|
||||
"name": "Media zewnętrzne",
|
||||
@@ -1745,7 +1745,7 @@
|
||||
"description": "Pozwala na automatyczną odpowiedź w tle. Notatka: To znacznie odsączy akumulator"
|
||||
},
|
||||
"cooldown_seconds": {
|
||||
"name": "Cooldown Seconds",
|
||||
"name": "Sekundy odnowienia",
|
||||
"description": "Minimalny czas między auto- odpowiedzi na tę samą rozmowę (w sekundach)"
|
||||
},
|
||||
"message_age_threshold": {
|
||||
@@ -1777,7 +1777,7 @@
|
||||
"description": "Klucz API do uwierzytelniania w serwisie AI"
|
||||
},
|
||||
"ai_system_prompt": {
|
||||
"name": "AI System Prompt",
|
||||
"name": "Prompt systemowy AI",
|
||||
"description": "System prompt, który definiuje osobowość i zachowanie AI"
|
||||
},
|
||||
"ai_max_tokens": {
|
||||
@@ -1847,7 +1847,7 @@
|
||||
"description": "Automatyczne odpowiedzi dla wiadomości czatu tekstowego"
|
||||
},
|
||||
"snap_messages": {
|
||||
"name": "Snap Replies",
|
||||
"name": "Odpowiedzi na Snap",
|
||||
"description": "Automatyczne odpowiedzi na zatrzaski"
|
||||
},
|
||||
"story_share_messages": {
|
||||
@@ -1871,7 +1871,7 @@
|
||||
"description": "Automatyczne odpowiedzi na naklejki"
|
||||
},
|
||||
"tiny_snap_messages": {
|
||||
"name": "Tiny Snap Replies",
|
||||
"name": "Odpowiedzi na ma?e Snapy",
|
||||
"description": "Automatyczne odpowiedzi na małe trzaski"
|
||||
},
|
||||
"map_reaction_messages": {
|
||||
@@ -1895,11 +1895,11 @@
|
||||
"description": "Pozwala Auto Open Pstryknięcia uruchomić w tle. Notatka: To znacznie odsączy akumulator"
|
||||
},
|
||||
"min_delay": {
|
||||
"name": "Min Delay (ms)",
|
||||
"name": "Min. op??nienie (ms)",
|
||||
"description": "Minimalne opóźnienie w milisekundach przed otwarciem zatrzasku"
|
||||
},
|
||||
"max_delay_ms": {
|
||||
"name": "Max Delay (ms)",
|
||||
"name": "Maks. op??nienie (ms)",
|
||||
"description": "Maksymalne opóźnienie w milisekundach przed otwarciem zatrzasku"
|
||||
},
|
||||
"queue_size": {
|
||||
@@ -1991,7 +1991,7 @@
|
||||
"description": "Zatrzymaj tłumaczenie, gdy usługa jest zablokowana"
|
||||
},
|
||||
"max_retries": {
|
||||
"name": "Max Retries",
|
||||
"name": "Maks. liczba pr?b",
|
||||
"description": "Maksymalna liczba prób powtórnych"
|
||||
},
|
||||
"retry_delay": {
|
||||
@@ -2112,7 +2112,7 @@
|
||||
"description": "Cechy doświadczalne",
|
||||
"properties": {
|
||||
"native_hooks": {
|
||||
"name": "Native Hooks",
|
||||
"name": "Native hooks",
|
||||
"description": "Niebezpieczne funkcje, które podłączyć do natywnego kodu Snapchat",
|
||||
"properties": {
|
||||
"composer_hooks": {
|
||||
@@ -2180,7 +2180,7 @@
|
||||
"description": "Override Android ID wysłany do Snapchat",
|
||||
"properties": {
|
||||
"spoof_android_id": {
|
||||
"name": "Spoof Android ID",
|
||||
"name": "Podszyj Android ID",
|
||||
"description": "Przekrocz ID Android wysłany do Snapchat z wartością niestandardową"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -2402,7 +2402,7 @@
|
||||
"FAMILY_CENTER_ACCEPT": "Zaakceptuj Centrum Rodzinne",
|
||||
"FAMILY_CENTER_LEAVE": "Centrum Rodzinne",
|
||||
"STATUS_PLUS_GIFT": "Status Plus prezent",
|
||||
"TINY_SNAP": "Tiny Snap",
|
||||
"TINY_SNAP": "Ma?y Snap",
|
||||
"STATUS_COUNTDOWN": "Odliczanie",
|
||||
"MAP_REACTION": "Mapa Reakcja",
|
||||
"chat_messages": "Wiadomości czatowe",
|
||||
@@ -2412,9 +2412,9 @@
|
||||
"external_media_messages": "Media zewnętrzne",
|
||||
"voice_note_messages": "Notatka głosowa",
|
||||
"sticker_messages": "Naklejka",
|
||||
"tiny_snap_messages": "Tiny Snap",
|
||||
"tiny_snap_messages": "Ma?y Snap",
|
||||
"map_reaction_messages": "Mapa Reakcja",
|
||||
"half_swipes": "Half Swipes"
|
||||
"half_swipes": "Po?owiczne przesuni?cia"
|
||||
},
|
||||
"media_download_source": {
|
||||
"none": "Brak",
|
||||
@@ -2514,8 +2514,8 @@
|
||||
"actions": {
|
||||
"remove_friends": "Usuń znajomych",
|
||||
"clear_conversations": "Wyczyść konwersje",
|
||||
"clear_friend_feed": "Clear Friend Feed ({count})",
|
||||
"unfollow": "Unfollow",
|
||||
"clear_friend_feed": "Wyczy?? kana? znajomych ({count})",
|
||||
"unfollow": "Przesta? obserwowa?",
|
||||
"remove": "Usuń"
|
||||
},
|
||||
"leave_groups": "Wyjdź{count}grupy",
|
||||
@@ -2622,7 +2622,7 @@
|
||||
"snap_replayed": "Snap replained",
|
||||
"snap_replayed_twice": "Snap ponownie dwukrotnie",
|
||||
"snap_screenshot": "Zrzut ekranu",
|
||||
"snap_screen_record": "Snap Screen Record",
|
||||
"snap_screen_record": "Nagranie ekranu Snap",
|
||||
"i_can_see_you": "Widzę cię"
|
||||
},
|
||||
"cleared_from_feed": "Oczyszczone z paszy",
|
||||
@@ -2650,7 +2650,7 @@
|
||||
"dialog_message": "Na pewno chcesz zadzwonić?"
|
||||
},
|
||||
"half_swipe_notifier": {
|
||||
"notification_channel_name": "Half Swipe",
|
||||
"notification_channel_name": "P??przesuni?cie",
|
||||
"notification_content_dm": "{friend}po prostu połowicznie wciśnięty do czatu{duration}sekund",
|
||||
"notification_content_group": "{friend}po prostu połowicznie{group}zamiast{duration}sekund"
|
||||
},
|
||||
@@ -2728,7 +2728,7 @@
|
||||
"error_channel_description": "Powiadomienia o błędach w przypadku awarii automatycznego otwierania",
|
||||
"paused_status": "Automatyczne otwieranie zamków",
|
||||
"processing_status": "Przetwarzanie pęknięć:{queued}w kolejce,{processed}przetworzone",
|
||||
"monitor_status": "Monitoring...",
|
||||
"monitor_status": "Monitorowanie...",
|
||||
"recent_snaps": "Ostatnie trzaski",
|
||||
"action_pause": "Pauza",
|
||||
"action_resume": "Wznowienie",
|
||||
@@ -2739,7 +2739,7 @@
|
||||
"paused_feedback": "Automatyczne otwieranie",
|
||||
"resumed_message": "Przetwarzanie będzie kontynuowane automatycznie w kolejce zatrzasków",
|
||||
"paused_message": "Przetwarzanie przerwane. Konserwacja kolejki ({count})",
|
||||
"status_paused": "Paused",
|
||||
"status_paused": "Wstrzymane",
|
||||
"status_monitoring": "Monitorowanie",
|
||||
"status_active": "Aktywne",
|
||||
"queue_cleared": "Kolejka oczyszczone i statystyki reset",
|
||||
@@ -2751,7 +2751,7 @@
|
||||
"unknown_user": "Nieznany użytkownik",
|
||||
"content_type_external_media": "Media zewnętrzne",
|
||||
"content_type_snap": "Snap",
|
||||
"conversation_type_friend_dm": "Friend DM",
|
||||
"conversation_type_friend_dm": "DM znajomego",
|
||||
"conversation_type_dm": "DM",
|
||||
"conversation_type_group_chat": "Grupa czat",
|
||||
"conversation_type_chat": "Rozmowa",
|
||||
@@ -2883,7 +2883,7 @@
|
||||
"translation_position": {
|
||||
"above": "Powyżej",
|
||||
"below": "Poniżej",
|
||||
"inline": "Inline"
|
||||
"inline": "W linii"
|
||||
},
|
||||
"language_codes": {
|
||||
"en": "Angielski",
|
||||
@@ -2939,7 +2939,7 @@
|
||||
"placeholder": "Szukaj"
|
||||
},
|
||||
"filters": {
|
||||
"newest_first": "Newest first",
|
||||
"newest_first": "Najnowsze najpierw",
|
||||
"pick_a_date": "Wybierz datę",
|
||||
"title": "Filtry",
|
||||
"search_by": "Szukaj",
|
||||
@@ -3064,7 +3064,7 @@
|
||||
}
|
||||
},
|
||||
"ui_settings_title": "Ustawienia interfejsu użytkownika",
|
||||
"haptic_feedback_label": "Haptic Feedback",
|
||||
"haptic_feedback_label": "Haptyczne sprz??enie zwrotne",
|
||||
"updates_title": "Aktualizacje",
|
||||
"auto_update_check": "Automatyczne sprawdzanie aktualizacji",
|
||||
"update_check_frequency_daily": "Codziennie",
|
||||
@@ -3111,7 +3111,7 @@
|
||||
"import_script_from_url": "Importuj skrypt z URL",
|
||||
"warning_imported_scripts": "Ostrzeżenie: Importowane skrypty mogą być szkodliwe dla urządzenia. Import skryptów tylko z zaufanych źródeł.",
|
||||
"enter_url_here": "Podaj adres URL tutaj:",
|
||||
"import": "Import",
|
||||
"import": "Importuj",
|
||||
"cancel": "Anuluj",
|
||||
"documentation": "Dokumentacja"
|
||||
},
|
||||
@@ -3127,7 +3127,7 @@
|
||||
"no_friends_found": "Nie znaleziono przyjaciół",
|
||||
"exporting_memories": "Eksportowanie wspomnień... ({failed}nieudany)"
|
||||
},
|
||||
"clear_friend_feed": "Clear Friend Feed",
|
||||
"clear_friend_feed": "Wyczy?? kana? znajomych",
|
||||
"select_date": "Wybierz datę",
|
||||
"schedule_scheduled_for": "Planowane{name}w{time}",
|
||||
"schedule_sending_in": "Wysyłanie{time}",
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Selecionar idioma",
|
||||
@@ -182,7 +182,7 @@
|
||||
"rules_title": "Regras",
|
||||
"participants_text": "{count}participantes",
|
||||
"not_found": "Não encontrado",
|
||||
"streaks_title": "Streaks",
|
||||
"streaks_title": "Sequ?ncias",
|
||||
"streaks_length_text": "Comprimento:{length}",
|
||||
"streaks_expiration_text": "Expira em{eta}",
|
||||
"streaks_expiration_text_expired": "Expirado",
|
||||
@@ -305,7 +305,7 @@
|
||||
"clear_module_data_failed": "Falha ao limpar os dados do módulo",
|
||||
"delete_module_button": "Apagar",
|
||||
"delete_module_failed": "Falha ao remover o módulo",
|
||||
"documentation_button": "Docs",
|
||||
"documentation_button": "Documenta??o",
|
||||
"download_script_failed": "Falha ao baixar o script",
|
||||
"downloading_script": "A transferir o programa...",
|
||||
"edit_module_button": "Editar",
|
||||
@@ -716,7 +716,7 @@
|
||||
"notifications": {
|
||||
"chat_screenshot": "Imagem",
|
||||
"chat_screen_record": "Ecrã Gravação",
|
||||
"snap_replay": "Snap Replay",
|
||||
"snap_replay": "Reprodu??o de Snap",
|
||||
"camera_roll_save": "Gravar o Rolo da Câmara",
|
||||
"chat": "Conversar",
|
||||
"chat_reply": "Responder à Conversa",
|
||||
@@ -1021,7 +1021,7 @@
|
||||
"friendly": "Amigável",
|
||||
"humorous": "Humoroso",
|
||||
"empathetic": "Empático",
|
||||
"toxic": "Edgy",
|
||||
"toxic": "Agressivo",
|
||||
"busy": "Ocupado"
|
||||
},
|
||||
"ai_temperature": {
|
||||
@@ -1773,7 +1773,7 @@
|
||||
"description": "Modelo de IA a usar para gerar respostas (por exemplo, gpt-3.5-turbo, gpt-4)"
|
||||
},
|
||||
"ai_api_key": {
|
||||
"name": "AI API Key",
|
||||
"name": "Chave da API de IA",
|
||||
"description": "Chave API para autenticação com o serviço de IA"
|
||||
},
|
||||
"ai_system_prompt": {
|
||||
@@ -1817,7 +1817,7 @@
|
||||
"description": "Usar mensagens de modelo se IA não gerar uma resposta"
|
||||
},
|
||||
"ai_request_timeout": {
|
||||
"name": "AI Request Timeout",
|
||||
"name": "Tempo limite da solicita??o de IA",
|
||||
"description": "Tempo máximo para esperar pela resposta de IA (em segundos)"
|
||||
},
|
||||
"ai_retry_attempts": {
|
||||
@@ -2156,7 +2156,7 @@
|
||||
}
|
||||
},
|
||||
"spoof": {
|
||||
"name": "Spoof",
|
||||
"name": "Falsificar",
|
||||
"description": "Spoof várias informações sobre você",
|
||||
"properties": {
|
||||
"play_store_installer_package_name": {
|
||||
@@ -2180,7 +2180,7 @@
|
||||
"description": "Sobrescrever o ID Android enviado para o Snapchat",
|
||||
"properties": {
|
||||
"spoof_android_id": {
|
||||
"name": "Spoof Android ID",
|
||||
"name": "Falsificar ID do Android",
|
||||
"description": "Sobrescrever o ID Android enviado para o Snapchat com um valor personalizado"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -2401,7 +2401,7 @@
|
||||
"FAMILY_CENTER_INVITE": "Convidar para o Centro Familiar",
|
||||
"FAMILY_CENTER_ACCEPT": "Centro Familiar Aceitar",
|
||||
"FAMILY_CENTER_LEAVE": "Centro de Família",
|
||||
"STATUS_PLUS_GIFT": "Status Plus Gift",
|
||||
"STATUS_PLUS_GIFT": "Presente do Status Plus",
|
||||
"TINY_SNAP": "Pequeno Snap",
|
||||
"STATUS_COUNTDOWN": "Contagem regressiva",
|
||||
"MAP_REACTION": "Reacção do Mapa",
|
||||
@@ -2552,7 +2552,7 @@
|
||||
"suggested": "Sugerido",
|
||||
"deleted": "Apagado",
|
||||
"business_accounts": "Contas de Negócios",
|
||||
"streaks": "Streaks",
|
||||
"streaks": "Sequ?ncias",
|
||||
"non_streaks": "Não Streaks",
|
||||
"followed": "Seguido",
|
||||
"following": "Seguir",
|
||||
@@ -2562,7 +2562,7 @@
|
||||
"none": "Nenhum",
|
||||
"username": "Utilizador",
|
||||
"added_timestamp": "Marca de tempo adicionada",
|
||||
"snap_score": "Snap Score",
|
||||
"snap_score": "Pontua??o do Snap",
|
||||
"streak_length": "Comprimento do Streak",
|
||||
"most_messages_sent": "A maioria das mensagens enviadas",
|
||||
"most_recent_message": "Mensagem mais recente",
|
||||
@@ -2685,7 +2685,7 @@
|
||||
}
|
||||
},
|
||||
"streaks_reminder": {
|
||||
"notification_title": "Streaks",
|
||||
"notification_title": "Sequ?ncias",
|
||||
"notification_text": "Vais perder o teu Streak com{friend}em{hoursLeft}horas"
|
||||
},
|
||||
"biometric_auth": {
|
||||
@@ -2729,7 +2729,7 @@
|
||||
"paused_status": "Abrir automaticamente os Snaps pausados",
|
||||
"processing_status": "Processamento de encaixes:{queued}na fila,{processed}processado",
|
||||
"monitor_status": "A monitorizar...",
|
||||
"recent_snaps": "Recent Snaps",
|
||||
"recent_snaps": "Snaps recentes",
|
||||
"action_pause": "Pausa",
|
||||
"action_resume": "Continuar",
|
||||
"action_clear": "Limpar fila",
|
||||
@@ -2967,7 +2967,7 @@
|
||||
"message_reaction_add": "Reacção adicionada",
|
||||
"message_reaction_remove": "Reacção removida",
|
||||
"snap_opened": "Abrir o encaixe",
|
||||
"snap_replayed": "Replayed snap",
|
||||
"snap_replayed": "Reproduziu um snap",
|
||||
"snap_replayed_twice": "Replayed snap duas vezes",
|
||||
"snap_screenshot": "Tirar uma imagem",
|
||||
"snap_screen_record": "Tela gravada"
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Alegeți limba",
|
||||
@@ -115,7 +115,7 @@
|
||||
"available_tabs_title": "Taburi disponibile",
|
||||
"reset_button": "Reinițializează",
|
||||
"done_button": "Gata",
|
||||
"clear_friend_feed": "Clear Friend Feed",
|
||||
"clear_friend_feed": "Cur??? fluxul de prieteni",
|
||||
"test_mode_label": "Activează PurrAura",
|
||||
"disable_feature_loading_label": "Dezactivează încărcarea caracteristicilor",
|
||||
"disable_auto_mapper_label": "Dezactivează Maper automat",
|
||||
@@ -663,16 +663,17 @@
|
||||
"auto_save": "Mesaje de salvare automată",
|
||||
"unsaveable_messages": " Mesaje nesalvabile",
|
||||
"auto_open_snaps": "Snaps Open Auto",
|
||||
"stealth": "👻 Stealth Mode",
|
||||
"stealth": "?? Mod invizibil",
|
||||
"auto_reply": "Răspuns automat",
|
||||
"auto_delete_sent_messages": "Sterge automat mesajele trimise",
|
||||
"mark_snaps_as_seen": " Mark Snaps așa cum se vede",
|
||||
"mark_stories_as_seen_locally": " Mark Stories as seen local",
|
||||
"conversation_info": "Informaţii de conversaţie",
|
||||
"e2e_encryption": "Utilizați criptarea E2E",
|
||||
"message_logger": "📝 Message Logger",
|
||||
"auto_read": "✅ Auto Read",
|
||||
"hide_typing_indicator": "🙈 Hide Typing Indicator"
|
||||
"message_logger": "?? Jurnal mesaje",
|
||||
"auto_read": "? Citire automat?",
|
||||
"hide_typing_indicator": "?? Ascunde indicatorul de tastare",
|
||||
"stealth_mode": "?? Mod invizibil"
|
||||
},
|
||||
"schedule_scheduled_for": "Programat pentru{name}în{time}",
|
||||
"schedule_sending_in": "Trimit{time}",
|
||||
@@ -755,7 +756,8 @@
|
||||
"stealth": {
|
||||
"blacklist": "Lista neagră",
|
||||
"whitelist": "Lista albă",
|
||||
"disabled": "Dezactivat"
|
||||
"disabled": "Dezactivat",
|
||||
"stealth": "?? Mod invizibil"
|
||||
},
|
||||
"auto_save": {
|
||||
"blacklist": "Lista neagră",
|
||||
@@ -892,13 +894,13 @@
|
||||
},
|
||||
"snapchat_plus": {
|
||||
"not_subscribed": "Neabonat",
|
||||
"basic": "Basic",
|
||||
"basic": "De baz?",
|
||||
"ad_free": "Ad free",
|
||||
"null": "Implicit"
|
||||
},
|
||||
"bypass_video_length_restriction": {
|
||||
"single": "Medii unice",
|
||||
"split": "Split media",
|
||||
"split": "?mparte media",
|
||||
"null": "Implicit"
|
||||
},
|
||||
"old_bitmoji_selfie": {
|
||||
@@ -909,7 +911,7 @@
|
||||
"disable_confirmation_dialogs": {
|
||||
"erase_message": "Șterge mesajul",
|
||||
"remove_friend": "Elimină prietenul",
|
||||
"block_friend": "Block Friend",
|
||||
"block_friend": "Blocheaz? prieten",
|
||||
"ignore_friend": "Ignoră prietenul",
|
||||
"hide_friend": "Ascunde prietenul",
|
||||
"hide_conversation": "Ascunde conversația",
|
||||
@@ -969,7 +971,7 @@
|
||||
"auto_mark_as_read": {
|
||||
"conversation_read": "Marchează conversația ca citită atunci când trimiți un mesaj",
|
||||
"snap_reply": "Marca se fixează ca citit atunci când răspunde la ele",
|
||||
"save_snap_in_chat": "Mark plesnește ca citit atunci când le salva în chat în timp ce în Stealth Mode"
|
||||
"save_snap_in_chat": "Marcheaz? snapurile ca v?zute c?nd le salvezi ?n chat ?n timp ce e?ti ?n Modul invizibil"
|
||||
},
|
||||
"friend_mutation_notifier": {
|
||||
"remove_friend": "Anunţă când cineva te elimină ca prieten",
|
||||
@@ -1016,7 +1018,7 @@
|
||||
"friendly, casual, helpful, empathetic": "prietenos, ocazional, util, empatic"
|
||||
},
|
||||
"ai_response_style": {
|
||||
"casual": "Casual",
|
||||
"casual": "Relaxat",
|
||||
"formal": "Formal",
|
||||
"friendly": "Prietenos",
|
||||
"humorous": "Umor",
|
||||
@@ -1092,7 +1094,7 @@
|
||||
"sticker_messages": "Autocolante",
|
||||
"tiny_snap_messages": "Snaps mici",
|
||||
"map_reaction_messages": "Hartă Reacţii",
|
||||
"half_swipes": "Half Swipes"
|
||||
"half_swipes": "Glis?ri pe jum?tate"
|
||||
},
|
||||
"supported_languages": {
|
||||
"en": "Engleză",
|
||||
@@ -1112,7 +1114,7 @@
|
||||
"translation_position": {
|
||||
"above": "Deasupra textului",
|
||||
"below": "Sub text",
|
||||
"inline": "Inline"
|
||||
"inline": "?n linie"
|
||||
},
|
||||
"source_language": {
|
||||
"auto": "Detectează automat"
|
||||
@@ -1139,7 +1141,7 @@
|
||||
"description": "Setează coordonatele locației spoofed"
|
||||
},
|
||||
"walk_radius": {
|
||||
"name": "Walk Radius",
|
||||
"name": "Raz? de mers",
|
||||
"description": "Plimbare aleatoare în jurul valorii de pe această rază (ft)"
|
||||
},
|
||||
"always_update_location": {
|
||||
@@ -1173,7 +1175,7 @@
|
||||
"description": "Suprascrie calitatea de încărcare media",
|
||||
"properties": {
|
||||
"force_video_upload_source_quality": {
|
||||
"name": "Force Video Upload Source Quality",
|
||||
"name": "For?eaz? calitatea sursei de ?nc?rcare video",
|
||||
"description": "Forţează Snapchat să folosească calitatea sursei la încărcarea videoclipurilor\nVă rugăm să rețineți că acest lucru nu poate elimina metadatele din mass-media"
|
||||
},
|
||||
"disable_image_compression": {
|
||||
@@ -1341,7 +1343,7 @@
|
||||
"description": "Cantitatea de fire de utilizat"
|
||||
},
|
||||
"preset": {
|
||||
"name": "Preset",
|
||||
"name": "Presetare",
|
||||
"description": "Setează viteza de conversie"
|
||||
},
|
||||
"constant_rate_factor": {
|
||||
@@ -1349,7 +1351,7 @@
|
||||
"description": "Setează factorul de rată constantă pentru codorul video\nDe la 0 la 51 pentru libx264"
|
||||
},
|
||||
"video_bitrate": {
|
||||
"name": "Video Bitrate",
|
||||
"name": "Bitrate video",
|
||||
"description": "Setează rata de biți video (kbps)"
|
||||
},
|
||||
"audio_bitrate": {
|
||||
@@ -1449,7 +1451,7 @@
|
||||
"description": "Arată informații utile ale mass-media, cum ar fi data creării în meniul contextului vizualizatorului operei"
|
||||
},
|
||||
"old_bitmoji_selfie": {
|
||||
"name": "Old Bitmoji Selfie",
|
||||
"name": "Selfie Bitmoji vechi",
|
||||
"description": "Aduce înapoi selfie-urile Bitmoji din versiunile vechi Snapchat"
|
||||
},
|
||||
"disable_spotlight": {
|
||||
@@ -1847,11 +1849,11 @@
|
||||
"description": "Mesaje de răspuns automat pentru mesajele de chat text"
|
||||
},
|
||||
"snap_messages": {
|
||||
"name": "Snap Replies",
|
||||
"name": "R?spunsuri la Snap",
|
||||
"description": "Mesaje de răspuns automat pentru capturi"
|
||||
},
|
||||
"story_share_messages": {
|
||||
"name": "Story Share Replies",
|
||||
"name": "R?spunsuri la partaj?ri de Story",
|
||||
"description": "Mesaje de răspuns automat pentru acțiuni de poveste"
|
||||
},
|
||||
"story_reply_messages": {
|
||||
@@ -1991,7 +1993,7 @@
|
||||
"description": "Pauză traducere atunci când serviciul este blocat"
|
||||
},
|
||||
"max_retries": {
|
||||
"name": "Max Retries",
|
||||
"name": "Num?r maxim de ?ncerc?ri",
|
||||
"description": "Numărul maxim de încercări de rejudecare"
|
||||
},
|
||||
"retry_delay": {
|
||||
@@ -2124,7 +2126,7 @@
|
||||
"description": "Arată primul nume de utilizator creat lângă numele de utilizator curent din pagina de profil"
|
||||
},
|
||||
"bypass_camera_roll_limit": {
|
||||
"name": "Bypass Camera Roll Limit",
|
||||
"name": "Ocole?te limita rolei foto",
|
||||
"description": "Crește cantitatea maximă de mass-media pe care o puteți trimite din rola camerei"
|
||||
},
|
||||
"custom_self_destruct_snap_delay": {
|
||||
@@ -2160,7 +2162,7 @@
|
||||
"description": "Spoof diverse informații despre tine",
|
||||
"properties": {
|
||||
"play_store_installer_package_name": {
|
||||
"name": "Play Store Installer Package Name",
|
||||
"name": "Nume pachet instalator Play Store",
|
||||
"description": "Suprascrie numele pachetului instalatorului la com.android.vending"
|
||||
},
|
||||
"remove_vpn_transport_flag": {
|
||||
@@ -2260,7 +2262,7 @@
|
||||
"description": "Încercarea de a repara meniul de alimentare Friend ca atunci când dispozitivul este offline nu poate fi afișat corect"
|
||||
},
|
||||
"app_lock": {
|
||||
"name": "App Lock",
|
||||
"name": "Blocare aplica?ie",
|
||||
"description": "Previne accesul la Snapchat fără cod de acces",
|
||||
"properties": {
|
||||
"lock_on_resume": {
|
||||
@@ -2380,7 +2382,7 @@
|
||||
"preview": "Previzualizare",
|
||||
"stealth_mode": "Mod ascuns",
|
||||
"auto_download_blacklist": "Descărcare automată Lista neagră",
|
||||
"anti_auto_save": "Anti Auto Save"
|
||||
"anti_auto_save": "Anti salvare automat?"
|
||||
},
|
||||
"content_type": {
|
||||
"CHAT": "Discută",
|
||||
@@ -2400,7 +2402,7 @@
|
||||
"CREATIVE_TOOL_ITEM": "Element instrument creativ",
|
||||
"FAMILY_CENTER_INVITE": "Family Center Invită",
|
||||
"FAMILY_CENTER_ACCEPT": "Centrul de Familie Acceptă",
|
||||
"FAMILY_CENTER_LEAVE": "Family Center Leave",
|
||||
"FAMILY_CENTER_LEAVE": "P?r?se?te Family Center",
|
||||
"STATUS_PLUS_GIFT": "Stare Plus cadou",
|
||||
"TINY_SNAP": "Snap mic",
|
||||
"STATUS_COUNTDOWN": "Numărătoarea inversă",
|
||||
@@ -2414,7 +2416,7 @@
|
||||
"sticker_messages": "Autocolant",
|
||||
"tiny_snap_messages": "Snap mic",
|
||||
"map_reaction_messages": "Reacţie pe hartă",
|
||||
"half_swipes": "Half Swipes"
|
||||
"half_swipes": "Glis?ri pe jum?tate"
|
||||
},
|
||||
"media_download_source": {
|
||||
"none": "Niciuna",
|
||||
@@ -2619,8 +2621,8 @@
|
||||
"message_reaction_add": "Reacție mesaj Adaugă",
|
||||
"message_reaction_remove": "Elimină reacția mesajului",
|
||||
"snap_opened": "Snap Deschis",
|
||||
"snap_replayed": "Snap Replayed",
|
||||
"snap_replayed_twice": "Snap Replayed Twice",
|
||||
"snap_replayed": "Snap reluat",
|
||||
"snap_replayed_twice": "Snap reluat de dou? ori",
|
||||
"snap_screenshot": "Captură de ecran",
|
||||
"snap_screen_record": "Snap înregistrare ecran",
|
||||
"i_can_see_you": "Te pot vedea"
|
||||
@@ -2650,7 +2652,7 @@
|
||||
"dialog_message": "Sigur vrei să începi un apel?"
|
||||
},
|
||||
"half_swipe_notifier": {
|
||||
"notification_channel_name": "Half Swipe",
|
||||
"notification_channel_name": "Glisare pe jum?tate",
|
||||
"notification_content_dm": "{friend}doar pe jumătate băgat în discuţia ta pentru{duration}secunde",
|
||||
"notification_content_group": "{friend}doar pe jumătate lovit în{group}pentru{duration}secunde"
|
||||
},
|
||||
@@ -2759,7 +2761,7 @@
|
||||
"notification_statistics": "STATISTICI",
|
||||
"notification_queue_size": "Mărime coadă",
|
||||
"notification_total_opened": "Total capturi deschise",
|
||||
"notification_queue_preview": "QUEUE PREVIEW",
|
||||
"notification_queue_preview": "PREVIZUALIZARE COAD?",
|
||||
"notification_processing_continue": "Procesarea va continua automat...",
|
||||
"notification_no_snaps_queue": "Fără pocniri la coadă.",
|
||||
"notification_queue_cleared_opened": "Coada este închisă ({opened}deschis)",
|
||||
@@ -2883,7 +2885,7 @@
|
||||
"translation_position": {
|
||||
"above": "Deasupra",
|
||||
"below": "Mai jos",
|
||||
"inline": "Inline"
|
||||
"inline": "?n linie"
|
||||
},
|
||||
"language_codes": {
|
||||
"en": "Engleză",
|
||||
@@ -3127,7 +3129,7 @@
|
||||
"no_friends_found": "Nu am găsit niciun prieten",
|
||||
"exporting_memories": "Export amintiri... ({failed}a eșuat)"
|
||||
},
|
||||
"clear_friend_feed": "Clear Friend Feed",
|
||||
"clear_friend_feed": "Cur??? fluxul de prieteni",
|
||||
"select_date": "Alegeți data",
|
||||
"schedule_scheduled_for": "Programat pentru{name}în{time}",
|
||||
"schedule_sending_in": "Trimit{time}",
|
||||
@@ -3182,7 +3184,7 @@
|
||||
"failed_to_edit_message": "Eșec la editarea mesajului:{error}"
|
||||
},
|
||||
"ai_response_style": {
|
||||
"casual": "Casual",
|
||||
"casual": "Relaxat",
|
||||
"formal": "Formal",
|
||||
"friendly": "Prietenos",
|
||||
"humorous": "Umor",
|
||||
@@ -3218,4 +3220,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Выберите язык",
|
||||
@@ -97,7 +97,7 @@
|
||||
"update_notification_channel_description": "Получить уведомление, когда новые релизы доступны",
|
||||
"update_notification_title": "Новое обновление доступно",
|
||||
"update_notification_text": "Нажмите, чтобы открыть PurrfectSnap и загрузить последнюю сборку.",
|
||||
"app_theme_title": "App Theme",
|
||||
"app_theme_title": "???? ??????????",
|
||||
"theme_icon_description": "Выбор открытой темы",
|
||||
"theme_mode_system": "Система",
|
||||
"theme_mode_light": "Свет",
|
||||
@@ -532,11 +532,11 @@
|
||||
}
|
||||
},
|
||||
"auto_open_snaps": {
|
||||
"name": "Auto Open Snaps",
|
||||
"name": "???????????? ??????",
|
||||
"description": "Автоматически открывает Snaps при их получении",
|
||||
"options": {
|
||||
"blacklist": "Скачать Auto Open Snaps",
|
||||
"whitelist": "Auto Open Snaps"
|
||||
"whitelist": "???????????? ??????"
|
||||
}
|
||||
},
|
||||
"hide_friend_feed": {
|
||||
@@ -611,7 +611,7 @@
|
||||
"description": "Экспорт памяти в ZIP-файл"
|
||||
},
|
||||
"bulk_messaging_action": {
|
||||
"name": "Bulk Messaging Action",
|
||||
"name": "???????? ???????? ?????????",
|
||||
"description": "Выполняет такие операции, как удаление друзей или массовое удаление разговоров"
|
||||
},
|
||||
"regen_mappings": {
|
||||
@@ -833,7 +833,7 @@
|
||||
"null": "Кодек по умолчанию"
|
||||
},
|
||||
"preset": {
|
||||
"null": "Default Preset"
|
||||
"null": "????????????? ?? ?????????"
|
||||
},
|
||||
"app_appearance_override": {
|
||||
"title": "Внешний вид"
|
||||
@@ -893,7 +893,7 @@
|
||||
"snapchat_plus": {
|
||||
"not_subscribed": "Не подписывается",
|
||||
"basic": "Базовый",
|
||||
"ad_free": "Ad Free",
|
||||
"ad_free": "??? ???????",
|
||||
"null": "По умолчанию"
|
||||
},
|
||||
"bypass_video_length_restriction": {
|
||||
@@ -1395,7 +1395,7 @@
|
||||
}
|
||||
},
|
||||
"snap_preview": {
|
||||
"name": "Snap Preview",
|
||||
"name": "???????????? ?????",
|
||||
"description": "Отображает небольшой предварительный просмотр рядом с невидимыми Snaps в чате"
|
||||
},
|
||||
"bootstrap_override": {
|
||||
@@ -1597,7 +1597,7 @@
|
||||
"description": "Показывает предварительный просмотр полученных сообщений в уведомлении"
|
||||
},
|
||||
"media_preview": {
|
||||
"name": "Media Preview",
|
||||
"name": "???????????? ?????",
|
||||
"description": "Показывает предварительный просмотр выбранных типов медиа в уведомлении"
|
||||
},
|
||||
"media_caption": {
|
||||
@@ -1761,7 +1761,7 @@
|
||||
"description": "Используйте ИИ для создания интеллектуальных автоматических реплик вместо шаблонных сообщений"
|
||||
},
|
||||
"ai_provider": {
|
||||
"name": "AI Provider",
|
||||
"name": "????????? ??",
|
||||
"description": "Выберите, какой сервис ИИ использовать для генерации ответов"
|
||||
},
|
||||
"ai_endpoint_url": {
|
||||
@@ -2260,7 +2260,7 @@
|
||||
"description": "Попробуйте отремонтировать меню Friend Feed, так как когда устройство отключено, оно не может отображаться правильно"
|
||||
},
|
||||
"app_lock": {
|
||||
"name": "App Lock",
|
||||
"name": "?????????? ??????????",
|
||||
"description": "Предотвращение доступа к Snapchat без пароля",
|
||||
"properties": {
|
||||
"lock_on_resume": {
|
||||
@@ -2396,7 +2396,7 @@
|
||||
"STATUS_CONVERSATION_CAPTURE_RECORD": "Запись с экрана",
|
||||
"STATUS_CALL_MISSED_VIDEO": "Пропущенный видеозвонок",
|
||||
"STATUS_CALL_MISSED_AUDIO": "Пропущенный аудиозвонок",
|
||||
"LIVE_LOCATION_SHARE": "Live Location Share",
|
||||
"LIVE_LOCATION_SHARE": "????? ?????? ? ?????????? ? ???????? ???????",
|
||||
"CREATIVE_TOOL_ITEM": "Креативный инструмент",
|
||||
"FAMILY_CENTER_INVITE": "Семейный центр приглашает",
|
||||
"FAMILY_CENTER_ACCEPT": "Семейный центр принимает",
|
||||
@@ -2720,7 +2720,7 @@
|
||||
"incoming_secret_message": "Твой друг только что принял твой открытый ключ. Нажмите ниже, чтобы принять секрет."
|
||||
},
|
||||
"auto_open_snaps": {
|
||||
"title": "Auto Open Snaps",
|
||||
"title": "???????????? ??????",
|
||||
"priority_title": "Auto Open Snaps (приоритет)",
|
||||
"error_title": "Auto Open Snaps (ошибки)",
|
||||
"channel_description": "Уведомления о статусе очереди после автоматического открытия",
|
||||
@@ -2842,7 +2842,7 @@
|
||||
},
|
||||
"material3_strings": {
|
||||
"date_range_picker_start_headline": "Из",
|
||||
"date_range_picker_end_headline": "To",
|
||||
"date_range_picker_end_headline": "????",
|
||||
"date_range_picker_title": "Выберите диапазон дат",
|
||||
"date_picker_switch_to_calendar_mode": "Расписание",
|
||||
"date_picker_switch_to_input_mode": "Ввод",
|
||||
@@ -3051,7 +3051,7 @@
|
||||
"clear": "Чисто",
|
||||
"files": {
|
||||
"config_json": "Конфигурационный файл",
|
||||
"mappings_json": "Mappings File",
|
||||
"mappings_json": "???? ?????????????",
|
||||
"message_logger_db": "База данных Message Logger",
|
||||
"pinned_best_friend_txt": "Файл лучшего друга",
|
||||
"native_sig_cache_txt": "Родная подпись Файл кэша"
|
||||
@@ -3074,7 +3074,7 @@
|
||||
"update_channel_prerelease": "Предварительный выпуск",
|
||||
"friend_notes_title": "Записки друзей",
|
||||
"friend_notes_description": "Управление и резервное копирование заметок друзей",
|
||||
"app_theme_title": "App Theme",
|
||||
"app_theme_title": "???? ??????????",
|
||||
"theme_mode_system": "Система",
|
||||
"theme_mode_light": "Свет",
|
||||
"theme_mode_dark": "Темный",
|
||||
@@ -3170,7 +3170,7 @@
|
||||
},
|
||||
"debug_dialogs": {
|
||||
"info": "Информация",
|
||||
"refs": "Refs",
|
||||
"refs": "??????",
|
||||
"arroyo": "Арро",
|
||||
"message": "Послание",
|
||||
"media_references": "Ссылки на СМИ",
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"dialogs": {
|
||||
"select_language": "Izberi jezik",
|
||||
@@ -546,7 +546,7 @@
|
||||
"name": "Uporabi šifriranje E2E"
|
||||
},
|
||||
"pin_conversation": {
|
||||
"name": "Pin Conversation"
|
||||
"name": "Pripni pogovor"
|
||||
},
|
||||
"exclude_message_logger": {
|
||||
"name": "Izbriši iz dnevnika sporočil"
|
||||
@@ -659,20 +659,21 @@
|
||||
"null": "Uporabi pravo raven baterije"
|
||||
},
|
||||
"friend_feed_menu_buttons": {
|
||||
"auto_download": "⬇️ Auto Download",
|
||||
"auto_save": "💬 Auto Save Messages",
|
||||
"unsaveable_messages": " Nerešljiva sporočila",
|
||||
"auto_open_snaps": "📷 Auto Open Snaps",
|
||||
"stealth": "👻 Stealth Mode",
|
||||
"auto_reply": "📨 Auto Reply",
|
||||
"auto_delete_sent_messages": "🗑️ Auto Delete Sent Messages",
|
||||
"mark_snaps_as_seen": " Mark Snaps, kot se vidi",
|
||||
"mark_stories_as_seen_locally": " Označite zgodbe, kot jih vidite na lokalni ravni",
|
||||
"conversation_info": "👤 Conversation Info",
|
||||
"e2e_encryption": "🔒 Use E2E Encryption",
|
||||
"message_logger": "📝 Message Logger",
|
||||
"auto_read": "✅ Auto Read",
|
||||
"hide_typing_indicator": "🙈 Hide Typing Indicator"
|
||||
"auto_download": "?? Samodejni prenos",
|
||||
"auto_save": "?? Samodejno shranjevanje sporo?il",
|
||||
"unsaveable_messages": "Nezapisljiva sporo?ila",
|
||||
"auto_open_snaps": "?? Samodejno odpiranje Snapov",
|
||||
"stealth": "?? Prikriti na?in",
|
||||
"auto_reply": "?? Samodejni odgovor",
|
||||
"auto_delete_sent_messages": "??? Samodejno brisanje poslanih sporo?il",
|
||||
"mark_snaps_as_seen": "Ozna?i Snape kot ogledane",
|
||||
"mark_stories_as_seen_locally": "Ozna?i Stories kot ogledane lokalno",
|
||||
"conversation_info": "?? Informacije o pogovoru",
|
||||
"e2e_encryption": "?? Uporabi E2E ?ifriranje",
|
||||
"message_logger": "?? Bele?enje sporo?il",
|
||||
"auto_read": "? Samodejno branje",
|
||||
"hide_typing_indicator": "?? Skrij indikator tipkanja",
|
||||
"auto_save_messages": "?? Samodejno shranjevanje sporo?il"
|
||||
},
|
||||
"schedule_scheduled_for": "Načrtovano za{name}v{time}",
|
||||
"schedule_sending_in": "Pošiljanje noter{time}",
|
||||
@@ -760,7 +761,8 @@
|
||||
"auto_save": {
|
||||
"blacklist": "Črna lista",
|
||||
"whitelist": "Bela lista",
|
||||
"disabled": "Onemogočeno"
|
||||
"disabled": "Onemogočeno",
|
||||
"auto_save": "?? Samodejno shranjevanje sporo?il"
|
||||
},
|
||||
"message_logger": {
|
||||
"blacklist": "Črna lista",
|
||||
@@ -1777,7 +1779,7 @@
|
||||
"description": "API ključ za preverjanje pristnosti z AI storitev"
|
||||
},
|
||||
"ai_system_prompt": {
|
||||
"name": "AI System Prompt",
|
||||
"name": "Poziv sistema AI",
|
||||
"description": "Sistem, ki določa osebnost in vedenje AI"
|
||||
},
|
||||
"ai_max_tokens": {
|
||||
@@ -1991,7 +1993,7 @@
|
||||
"description": "Prekini prevajanje, ko je storitev blokirana"
|
||||
},
|
||||
"max_retries": {
|
||||
"name": "Max Retries",
|
||||
"name": "Najve? ponovitev",
|
||||
"description": "Največje število poskusov ponovnega preskušanja"
|
||||
},
|
||||
"retry_delay": {
|
||||
@@ -2553,7 +2555,7 @@
|
||||
"deleted": "Zbrisano",
|
||||
"business_accounts": "Poslovni računi",
|
||||
"streaks": "Streaks",
|
||||
"non_streaks": "Non Streaks",
|
||||
"non_streaks": "Brez streakov",
|
||||
"followed": "Sledi",
|
||||
"following": "Sledi",
|
||||
"location_on_map": "Lokacija na zemljevidu"
|
||||
@@ -2562,7 +2564,7 @@
|
||||
"none": "Brez",
|
||||
"username": "Uporabniško ime",
|
||||
"added_timestamp": "Dodan časovni žig",
|
||||
"snap_score": "Snap Score",
|
||||
"snap_score": "Snap to?ke",
|
||||
"streak_length": "Dolžina streak",
|
||||
"most_messages_sent": "Večina poslanih sporočil",
|
||||
"most_recent_message": "Najnovejše sporočilo",
|
||||
@@ -3218,4 +3220,4 @@
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -702,7 +702,7 @@
|
||||
"append_type": "Додавання медіа типу до імені файлу"
|
||||
},
|
||||
"auto_download_sources": {
|
||||
"friend_snaps": "Friend Snaps",
|
||||
"friend_snaps": "Снепи друзів",
|
||||
"friend_stories": "Історія",
|
||||
"public_stories": "Громадські історії",
|
||||
"spotlight": "Мапа"
|
||||
@@ -2180,7 +2180,7 @@
|
||||
"description": "Ознайомитися з Android ID, надісланим на Snapchat",
|
||||
"properties": {
|
||||
"spoof_android_id": {
|
||||
"name": "Spoof Android ID",
|
||||
"name": "Підмінити Android ID",
|
||||
"description": "На відміну від Android ID, відправленого на Snapchat з індивідуальним значенням"
|
||||
},
|
||||
"custom_android_id": {
|
||||
@@ -2629,7 +2629,7 @@
|
||||
"tracker_actions": {
|
||||
"log": "Увійти",
|
||||
"in_app_notification": "Повідомлення",
|
||||
"push_notification": "Push Notification",
|
||||
"push_notification": "Push-сповіщення",
|
||||
"custom": "Написати"
|
||||
},
|
||||
"better_notifications": {
|
||||
@@ -3218,4 +3218,4 @@
|
||||
"openai": "Про компанію",
|
||||
"openrouter": "Проксимус"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ package me.eternal.purrfectsnap.common
|
||||
|
||||
object Constants {
|
||||
val SNAPCHAT_PACKAGE_NAME get() = "com.snapchat.android"
|
||||
val SE_PACKAGE_NAME get() = BuildConfig.APPLICATION_ID
|
||||
val MODULE_PACKAGE_NAME get() = BuildConfig.APPLICATION_ID
|
||||
const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ enum class FileHandleScope(
|
||||
INTERNAL("internal"),
|
||||
LOCALE("locale"),
|
||||
USER_IMPORT("user_import"),
|
||||
COMPOSER("composer");
|
||||
VALDI("valdi");
|
||||
|
||||
companion object {
|
||||
fun fromValue(name: String): FileHandleScope? = entries.find { it.key == name }
|
||||
|
||||
@@ -35,16 +35,19 @@ class ModConfig(
|
||||
|
||||
fun load() {
|
||||
wasPresent = fileWrapper.exists()
|
||||
root = createRootConfig().apply {
|
||||
if (!wasPresent) {
|
||||
writeConfigObject(this)
|
||||
return@apply
|
||||
}
|
||||
runCatching {
|
||||
loadConfig(this)
|
||||
}.onFailure {
|
||||
writeConfigObject(this)
|
||||
}
|
||||
val targetRoot = if (::root.isInitialized) {
|
||||
root
|
||||
} else {
|
||||
createRootConfig().also { root = it }
|
||||
}
|
||||
if (!wasPresent) {
|
||||
writeConfigObject(targetRoot)
|
||||
return
|
||||
}
|
||||
runCatching {
|
||||
loadConfig(targetRoot)
|
||||
}.onFailure {
|
||||
writeConfigObject(targetRoot)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,4 +139,4 @@ class ModConfig(
|
||||
root.fromJson(configObject)
|
||||
writeConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,16 +24,16 @@ class Experimental : ConfigContainer() {
|
||||
val notificationTranscript = boolean("notification_transcript") { requireRestart() }
|
||||
}
|
||||
|
||||
class ComposerHooksConfig: ConfigContainer(hasGlobalState = true) {
|
||||
class ValdiHooksConfig: ConfigContainer(hasGlobalState = true) {
|
||||
val showFirstCreatedUsername = boolean("show_first_created_username")
|
||||
val bypassCameraRollLimit = boolean("bypass_camera_roll_limit")
|
||||
val customSelfDestructSnapDelay = boolean("custom_self_destruct_snap_delay")
|
||||
val composerConsole = boolean("composer_console")
|
||||
val composerLogs = boolean("composer_logs")
|
||||
val valdiConsole = boolean("composer_console")
|
||||
val valdiLogs = boolean("composer_logs")
|
||||
}
|
||||
|
||||
class NativeHooks : ConfigContainer() {
|
||||
val composerHooks = container("composer_hooks", ComposerHooksConfig()) { requireRestart() }
|
||||
val valdiHooks = container("composer_hooks", ValdiHooksConfig()) { requireRestart() }
|
||||
val disableBitmoji = boolean("disable_bitmoji")
|
||||
val customEmojiFont = string("custom_emoji_font") {
|
||||
requireRestart()
|
||||
|
||||
@@ -61,10 +61,12 @@ class Global : ConfigContainer() {
|
||||
inner class UpdateSettings : ConfigContainer() {
|
||||
val autoUpdateCheck = boolean("auto_update_check")
|
||||
val updateCheckFrequency = unique("update_check_frequency", "daily", "weekly", "monthly")
|
||||
val updateChannel = unique("update_channel", "stable", "prerelease")
|
||||
}
|
||||
|
||||
inner class UISettings : ConfigContainer() {
|
||||
val hapticFeedback = boolean("haptic_feedback", true)
|
||||
val useSystemToasts = boolean("use_system_toasts", false)
|
||||
}
|
||||
|
||||
val updateSettings = container("update_settings", UpdateSettings()) { addFlags(ConfigFlag.HIDDEN) }
|
||||
|
||||
@@ -67,7 +67,7 @@ class MessagingTweaks : ConfigContainer() {
|
||||
|
||||
inner class AiConfig : ConfigContainer(hasGlobalState = false) {
|
||||
val enableAiReplies = boolean("enable_ai_replies", false)
|
||||
val aiProvider = unique("ai_provider", "gemini", "deepseek", "openai") {
|
||||
val aiProvider = unique("ai_provider", "gemini", "deepseek", "openai", "openrouter") {
|
||||
customOptionTranslationPath = "ai_provider"
|
||||
}.apply { set("gemini") }
|
||||
val aiModel = string("ai_model", defaultValue = "gemini-2.5-flash") {
|
||||
@@ -159,10 +159,6 @@ class MessagingTweaks : ConfigContainer() {
|
||||
}
|
||||
|
||||
class AutoOpenSnapsConfig : ConfigContainer(hasGlobalState = true) {
|
||||
init {
|
||||
globalState = false
|
||||
}
|
||||
|
||||
val allowRunningInBackground = boolean("allow_running_in_background", false)
|
||||
val minDelay = integer("min_delay", defaultValue = 50) {
|
||||
inputCheck = { it.toIntOrNull()?.coerceAtLeast(0) != null }
|
||||
@@ -223,6 +219,19 @@ class MessagingTweaks : ConfigContainer() {
|
||||
"EXTERNAL_MEDIA",
|
||||
"STICKER"
|
||||
) { requireRestart(); customOptionTranslationPath = "content_type" }
|
||||
|
||||
class UnsaveableMessagesConfig : ConfigContainer() {
|
||||
val chat = boolean("chat", defaultValue = true)
|
||||
val snap = boolean("snap")
|
||||
val externalMedia = boolean("external_media")
|
||||
val sticker = boolean("sticker")
|
||||
val share = boolean("share")
|
||||
val note = boolean("note")
|
||||
val storyReply = boolean("story_reply")
|
||||
}
|
||||
|
||||
val unsaveableMessages = container("unsaveable_messages", UnsaveableMessagesConfig()) { requireRestart() }
|
||||
|
||||
val preventMessageSending = multiple("prevent_message_sending", *NotificationType.getOutgoingValues().map { it.key }.toTypedArray()) {
|
||||
customOptionTranslationPath = "features.options.notifications"
|
||||
}
|
||||
@@ -239,7 +248,16 @@ class MessagingTweaks : ConfigContainer() {
|
||||
customOptionTranslationPath = "features.options.notifications"
|
||||
}
|
||||
val messageLogger = container("message_logger", MessageLoggerConfig()) { requireRestart() }
|
||||
val galleryMediaSendOverride = unique("gallery_media_send_override", "always_ask", "SNAP", "NOTE", "SAVEABLE_SNAP") { requireRestart() }
|
||||
|
||||
class GalleryMediaSendOverrideConfig : ConfigContainer() {
|
||||
val mode = unique("mode", "always_ask", "SNAP", "NOTE", "SAVEABLE_SNAP") {
|
||||
requireRestart()
|
||||
customOptionTranslationPath = "gallery_media_send_override"
|
||||
}
|
||||
val includeCameraSnaps = boolean("include_camera_snaps", false) { requireRestart() }
|
||||
}
|
||||
|
||||
val galleryMediaSendOverride = container("gallery_media_send_override", GalleryMediaSendOverrideConfig()) { requireRestart() }
|
||||
val scheduledSendAllowRunningInBackground = boolean("scheduled_send_allow_running_in_background", false)
|
||||
val stripMediaMetadata = multiple("strip_media_metadata", "hide_caption_text", "hide_snap_filters", "hide_extras", "remove_audio_note_duration", "remove_audio_note_transcript_capability") { requireRestart() }
|
||||
val bypassMessageRetentionPolicy = boolean("bypass_message_retention_policy") { addNotices(FeatureNotice.UNSTABLE); requireRestart() }
|
||||
@@ -286,3 +304,4 @@ class MessagingTweaks : ConfigContainer() {
|
||||
|
||||
val instantTranslation = container("instant_translation", InstantTranslationConfig()) { requireRestart() }
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,19 @@ import me.eternal.purrfectsnap.common.config.ConfigContainer
|
||||
import me.eternal.purrfectsnap.common.config.ConfigFlag
|
||||
|
||||
class Spoof : ConfigContainer(hasGlobalState = true) {
|
||||
inner class SpoofDeviceIdConfig : ConfigContainer() {
|
||||
val spoofAndroidId = boolean("spoof_android_id") { requireRestart() }
|
||||
val customAndroidId = string("custom_android_id") {
|
||||
requireRestart()
|
||||
inputCheck = { it.isEmpty() || (it.length == 16 && it.all { c -> c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F' }) }
|
||||
}
|
||||
}
|
||||
|
||||
val overridePlayStoreInstallerPackageName = boolean("play_store_installer_package_name") { requireRestart() }
|
||||
val removeVpnTransportFlag = boolean("remove_vpn_transport_flag") { requireRestart() }
|
||||
val removeMockLocationFlag = boolean("remove_mock_location_flag") { requireRestart() }
|
||||
val forceWifiTransportFlag = boolean("force_wifi_transport_flag") { requireRestart() }
|
||||
val spoofAndroidId = boolean("spoof_android_id") { requireRestart() }
|
||||
val customAndroidId = string("custom_android_id") {
|
||||
requireRestart()
|
||||
inputCheck = { it.isEmpty() || (it.length == 16 && it.all { c -> c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F' }) }
|
||||
}
|
||||
val spoofDeviceId = container("spoof_device_id", SpoofDeviceIdConfig()) { requireRestart() }
|
||||
val spoofDevice = boolean("spoof_device") { requireRestart() }
|
||||
val deviceModel = unique("device_model",
|
||||
"samsung_s25_ultra",
|
||||
|
||||
@@ -57,7 +57,7 @@ enum class MessagingRuleType(
|
||||
HIDE_FRIEND_FEED("hide_friend_feed", false, Icons.Outlined.VisibilityOff, showInFriendMenu = false),
|
||||
E2E_ENCRYPTION("e2e_encryption", false, Icons.Outlined.Lock),
|
||||
PIN_CONVERSATION("pin_conversation", false, Icons.Outlined.PushPin, showInFriendMenu = false),
|
||||
MESSAGE_LOGGER("message_logger", true, Icons.AutoMirrored.Filled.Message, showInFriendMenu = true),
|
||||
MESSAGE_LOGGER("message_logger", true, Icons.AutoMirrored.Filled.Message, showInFriendMenu = true, defaultValue = "blacklist"),
|
||||
AUTO_READ("auto_read", true, Icons.Outlined.DoneAll, defaultValue = "whitelist"),
|
||||
AUTO_REPLY("auto_reply", true, Icons.AutoMirrored.Outlined.Reply, defaultValue = "blacklist"),
|
||||
AUTO_DELETE_SENT_MESSAGES("auto_delete_sent_messages", true, Icons.Outlined.DeleteSweep, defaultValue = "blacklist");
|
||||
|
||||
@@ -52,7 +52,8 @@ class JSModule(
|
||||
putConst("description", this, moduleInfo.description)
|
||||
putConst("author", this, moduleInfo.author)
|
||||
putConst("minSnapchatVersion", this, moduleInfo.minSnapchatVersion)
|
||||
putConst("minSEVersion", this, moduleInfo.minSEVersion)
|
||||
putConst("minPSVersion", this, moduleInfo.minPSVersion)
|
||||
putConst("minSEVersion", this, moduleInfo.minPSVersion)
|
||||
putConst("grantedPermissions", this, moduleInfo.grantedPermissions)
|
||||
})
|
||||
})
|
||||
@@ -311,4 +312,4 @@ class JSModule(
|
||||
}
|
||||
} ?: "null"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ open class ScriptRuntime(
|
||||
val bufferedReader = content.bufferedReader()
|
||||
val moduleInfo = bufferedReader.readModuleInfo()
|
||||
|
||||
if (moduleInfo.minSEVersion != null && moduleInfo.minSEVersion > BuildConfig.VERSION_CODE) {
|
||||
throw Exception("Module requires a newer version of PurrfectSnap (min version: ${moduleInfo.minSEVersion})")
|
||||
if (moduleInfo.minPSVersion != null && moduleInfo.minPSVersion > BuildConfig.VERSION_CODE) {
|
||||
throw Exception("Module requires a newer version of PurrfectSnap (min version: ${moduleInfo.minPSVersion})")
|
||||
}
|
||||
|
||||
return JSModule(
|
||||
@@ -73,4 +73,4 @@ open class ScriptRuntime(
|
||||
modules[scriptPath] = this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ data class ModuleInfo(
|
||||
val updateUrl: String? = null,
|
||||
val author: String? = null,
|
||||
val minSnapchatVersion: Long? = null,
|
||||
val minSEVersion: Long? = null,
|
||||
val minPSVersion: Long? = null,
|
||||
val grantedPermissions: List<String>,
|
||||
val executionSides: List<String>? = null,
|
||||
) {
|
||||
@@ -23,14 +23,14 @@ data class ModuleInfo(
|
||||
|
||||
fun BufferedReader.readModuleInfo(): ModuleInfo {
|
||||
val header = readLine()
|
||||
if (!header.startsWith("// ==SE_module==")) {
|
||||
if (!header.startsWith("// ==PS_module==") && !header.startsWith("// ==SE_module==")) {
|
||||
throw Exception("Invalid module header")
|
||||
}
|
||||
|
||||
val properties = mutableMapOf<String, String>()
|
||||
while (true) {
|
||||
val line = readLine()
|
||||
if (line.startsWith("// ==/SE_module==")) {
|
||||
if (line.startsWith("// ==/PS_module==") || line.startsWith("// ==/SE_module==")) {
|
||||
break
|
||||
}
|
||||
val split = line.replaceFirst("//", "").split(":", limit = 2)
|
||||
@@ -52,7 +52,8 @@ fun BufferedReader.readModuleInfo(): ModuleInfo {
|
||||
updateUrl = properties["updateUrl"],
|
||||
author = properties["author"],
|
||||
minSnapchatVersion = properties["minSnapchatVersion"]?.toLongOrNull(),
|
||||
minSEVersion = properties["minSEVersion"]?.toLongOrNull(),
|
||||
minPSVersion = properties["minPSVersion"]?.toLongOrNull()
|
||||
?: properties["minSEVersion"]?.toLongOrNull(),
|
||||
grantedPermissions = properties["permissions"]?.split(",")?.map { it.trim() } ?: emptyList(),
|
||||
executionSides = properties["executionSides"]?.lowercase()?.split(",")?.map { it.trim() },
|
||||
)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export default {
|
||||
input: "./build/typescript/main.js",
|
||||
output: {
|
||||
file: "./build/loader.js",
|
||||
format: "iife",
|
||||
}
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { defineModule } from "../types";
|
||||
import { interceptComponent } from "../utils";
|
||||
|
||||
export default defineModule({
|
||||
name: "Bypass Camera Roll Selection Limit",
|
||||
enabled: config => config.bypassCameraRollLimit,
|
||||
init() {
|
||||
interceptComponent(
|
||||
'memories_ui/src/clickhandlers/MultiSelectClickHandler',
|
||||
'MultiSelectClickHandler',
|
||||
{
|
||||
"<init>": (args: any[], superCall: () => void) => {
|
||||
args[1].selectionLimit = 9999999;
|
||||
superCall();
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
});
|
||||
@@ -1,23 +1,35 @@
|
||||
{
|
||||
"allowed_eps_active": [
|
||||
"/messagingcoreservice.MessagingCoreService/",
|
||||
"/GetConvoSafetyPrompt",
|
||||
"/GetSnapchatterPublicInfo",
|
||||
"/UserRecentlyActive",
|
||||
"/socialsms.SocialSms/UpdateLink",
|
||||
"/snapchat.friending.server.RecentlyActive/UserRecentlyActive",
|
||||
"/snapchat.friending.server.ContactBook/",
|
||||
"/snapchat.friending.server.FriendRequests/",
|
||||
"/snapchat.friending.server.FriendAction/",
|
||||
"/com.snapchat.atlas.gw.AtlasGw/SyncFriendData",
|
||||
"/com.snapchat.atlas.gw.AtlasGw/GetFriendsUserScore",
|
||||
"/com.snapchat.atlas.gw.AtlasGw/GetUserRecentlyActive",
|
||||
"/snapchat.friending.server.FriendAction/",
|
||||
"/snapchat.friending.server.FriendRequests/",
|
||||
"/com.snapchat.atlas.gw.AtlasGw/SetUserDisplayName",
|
||||
"/snapchat.music.music_service.MusicService/",
|
||||
"/snapchat.music.external_music_service.ExternalMusicService/",
|
||||
"/snapchat.content.v2.MediaDeliveryService/",
|
||||
"/snapchat.content.v2.MediaOriginService/",
|
||||
"/snapchat.map.",
|
||||
"/snapchat.notif.DeviceStateReceiver/",
|
||||
"/snapchat.creativetools.",
|
||||
"/snapchat.lens.",
|
||||
"/snapchat.bitmoji.",
|
||||
"/snapchat.aura.api.AuraService/",
|
||||
"/com.snapchat.ads.notification.ProfileNotificationCRUDService/",
|
||||
"ClientIntegrityService",
|
||||
"/ClientIntegrityService",
|
||||
"/ClientIntegrityService/",
|
||||
"/GetAttestationHeaders",
|
||||
"/AttestationHeaders",
|
||||
"/getAttestationHeaders",
|
||||
"/GetAttestationPayload",
|
||||
"/AttestationPayload"
|
||||
"/com.snapchat.commerce.",
|
||||
"/snapchat.payments.commerce.",
|
||||
"/snapchat.abuse.support.AppealService/",
|
||||
"/snapchat.local.snapzen.userdata.",
|
||||
"/snapchat.valis.Valis/",
|
||||
"com.snap.identity.network.suggestion.BqSuggestFriendHttpInterface.fetchHighQualitySuggestedFriend",
|
||||
"/snap.security.ArgosService/GetTokens",
|
||||
"com.snap.playstate.net.ReadReceiptHttpInterface.batchUploadReadReceipts",
|
||||
"com.snap.identity.network.suggestion.BqSuggestFriendHttpInterface.fetchLegacySuggestedFriend",
|
||||
"com.snap.identity.FriendingHttpInterface.submitSuggestedFriendsAction"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation(project(":mapper"))
|
||||
implementation(project(":native"))
|
||||
implementation(project(":composer"))
|
||||
implementation(project(":valdi"))
|
||||
|
||||
implementation(libs.androidx.activity.ktx)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
@@ -45,4 +45,5 @@ dependencies {
|
||||
implementation(libs.androidx.material.icons.extended)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.hiddenapibypass)
|
||||
}
|
||||
implementation(libs.colorpicker.compose)
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
justify-content: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
margin: 5px 15px;
|
||||
--sender-color: #ffd3b6;
|
||||
}
|
||||
|
||||
main>.message .header {
|
||||
@@ -127,16 +128,9 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main>.message:nth-child(2n) .username {
|
||||
color: #dcedc1;
|
||||
}
|
||||
|
||||
main>.message:nth-child(2n + 1) .username {
|
||||
color: #ffd3b6;
|
||||
}
|
||||
|
||||
main>.message .username {
|
||||
font-weight: bold;
|
||||
color: var(--sender-color);
|
||||
}
|
||||
|
||||
main>.message .time {
|
||||
@@ -145,14 +139,6 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
main>.message:nth-child(2n) .content {
|
||||
border-color: #dcedc1;
|
||||
}
|
||||
|
||||
main>.message:nth-child(2n + 1) .content {
|
||||
border-color: #ffd3b6;
|
||||
}
|
||||
|
||||
main>.message .content {
|
||||
background-color: var(--Snap-sigBackgroundMessageSaved);
|
||||
border-left: 3px solid;
|
||||
@@ -160,6 +146,7 @@
|
||||
margin-top: 4px;
|
||||
padding-left: 4px;
|
||||
padding: 3px 0 3px 6px;
|
||||
border-color: var(--sender-color);
|
||||
}
|
||||
|
||||
main>.message .content div:has(.chat_media:not(audio):not(.overlay_media)) {
|
||||
@@ -203,6 +190,17 @@
|
||||
main>.message .red_snap_svg {
|
||||
color: var(--Snap-sigSnapWithoutSound);
|
||||
}
|
||||
main>.message .time.with_deleted {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
main>.message .deleted_icon_svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: var(--Snap-sigTextNegative);
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<header>
|
||||
@@ -220,6 +218,9 @@
|
||||
<svg class="red_snap_svg" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="5" width="10.5" height="10.5" rx="1.808" stroke="currentColor" stroke-width="1.5"></rect>
|
||||
</svg>
|
||||
<svg class="deleted_icon_svg" width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 3h6M5 7h14M6 7l1 14h10l1-14M10 10v7M14 10v7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -251,6 +252,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
if (!hex) return null
|
||||
let value = hex.replace("#", "")
|
||||
if (value.length === 3) {
|
||||
value = value.split("").map(c => c + c).join("")
|
||||
}
|
||||
if (value.length !== 6) return null
|
||||
const number = parseInt(value, 16)
|
||||
return {
|
||||
r: (number >> 16) & 255,
|
||||
g: (number >> 8) & 255,
|
||||
b: number & 255
|
||||
}
|
||||
}
|
||||
|
||||
function rgbToHsl(r, g, b) {
|
||||
r /= 255
|
||||
g /= 255
|
||||
b /= 255
|
||||
const max = Math.max(r, g, b)
|
||||
const min = Math.min(r, g, b)
|
||||
let h = 0
|
||||
let s = 0
|
||||
const l = (max + min) / 2
|
||||
const delta = max - min
|
||||
|
||||
if (delta !== 0) {
|
||||
s = delta / (1 - Math.abs(2 * l - 1))
|
||||
switch (max) {
|
||||
case r:
|
||||
h = ((g - b) / delta) % 6
|
||||
break
|
||||
case g:
|
||||
h = (b - r) / delta + 2
|
||||
break
|
||||
default:
|
||||
h = (r - g) / delta + 4
|
||||
break
|
||||
}
|
||||
h = Math.round(h * 60)
|
||||
if (h < 0) h += 360
|
||||
}
|
||||
|
||||
return { h, s: Math.round(s * 100), l: Math.round(l * 100) }
|
||||
}
|
||||
|
||||
function makeMain() {
|
||||
document.querySelector('main').innerHTML = ""
|
||||
const messageTemplate = document.querySelector("#message_template")
|
||||
@@ -260,9 +307,34 @@
|
||||
messageList = messageList.reverse()
|
||||
}
|
||||
|
||||
const userColors = new Map()
|
||||
const userColorsOverride = conversationData.userColors || {}
|
||||
const seedRgb = hexToRgb(conversationData.colorSeed)
|
||||
const baseHue = seedRgb ? rgbToHsl(seedRgb.r, seedRgb.g, seedRgb.b).h : null
|
||||
|
||||
function colorForUser(userId) {
|
||||
if (userColors.has(userId)) return userColors.get(userId)
|
||||
if (userColorsOverride[userId]) {
|
||||
userColors.set(userId, userColorsOverride[userId])
|
||||
return userColorsOverride[userId]
|
||||
}
|
||||
let hash = 0
|
||||
for (let i = 0; i < userId.length; i++) {
|
||||
hash = ((hash << 5) - hash) + userId.charCodeAt(i)
|
||||
hash |= 0
|
||||
}
|
||||
const hue = baseHue === null ? (Math.abs(hash) % 360) : ((baseHue + Math.abs(hash)) % 360)
|
||||
const color = `hsl(${hue}, 58%, 72%)`
|
||||
userColors.set(userId, color)
|
||||
return color
|
||||
}
|
||||
|
||||
messageList.forEach(message => {
|
||||
const messageObject = document.createElement("div")
|
||||
messageObject.classList.add("message")
|
||||
const participant = participants[message.senderId]
|
||||
const participantUserId = participant ? participant.userId : String(message.senderId)
|
||||
messageObject.style.setProperty("--sender-color", colorForUser(String(participantUserId)))
|
||||
|
||||
messageObject.appendChild(((headerElement) => {
|
||||
headerElement.classList.add("header")
|
||||
@@ -278,6 +350,10 @@
|
||||
headerElement.appendChild(((elem) => {
|
||||
elem.classList.add("time")
|
||||
elem.innerHTML = new Date(message.createdTimestamp).toUTCString()
|
||||
if (message.isDeleted || message.type === "STATUS") {
|
||||
elem.classList.add("with_deleted")
|
||||
elem.appendChild(document.querySelector('.deleted_icon_svg').cloneNode(true))
|
||||
}
|
||||
return elem
|
||||
})(document.createElement("div")))
|
||||
|
||||
@@ -384,4 +460,4 @@
|
||||
makeHeader()
|
||||
makeMain()
|
||||
</script>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
@@ -161,7 +161,7 @@ class ModContext(
|
||||
NativeConfig(
|
||||
disableBitmoji = config.experimental.nativeHooks.disableBitmoji.get(),
|
||||
disableMetrics = config.global.disableMetrics.get(),
|
||||
composerHooks = config.experimental.nativeHooks.composerHooks.globalState == true,
|
||||
valdiHooks = config.experimental.nativeHooks.valdiHooks.globalState == true,
|
||||
customEmojiFontPath = getCustomEmojiFontPath(this)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -210,7 +210,6 @@ class PurrfectSnap {
|
||||
}
|
||||
}
|
||||
|
||||
private var safeMode = false
|
||||
|
||||
private fun triggerMappingsGeneration() {
|
||||
runCatching {
|
||||
@@ -221,7 +220,7 @@ class PurrfectSnap {
|
||||
}
|
||||
|
||||
val intent = Intent().apply {
|
||||
setClassName(Constants.SE_PACKAGE_NAME, "${Constants.SE_PACKAGE_NAME}.ui.setup.SetupActivity")
|
||||
setClassName(Constants.MODULE_PACKAGE_NAME, "${Constants.MODULE_PACKAGE_NAME}.ui.setup.SetupActivity")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
putExtra("requirements", 4) // Requirements.MAPPINGS = 4
|
||||
}
|
||||
@@ -233,24 +232,12 @@ class PurrfectSnap {
|
||||
}
|
||||
|
||||
private fun onActivityCreate(activity: Activity) {
|
||||
if (!appContext.native.verifyKey(BuildConfig.NATIVE_KEY)) {
|
||||
safeMode = true
|
||||
}
|
||||
measureTimeMillis {
|
||||
with(appContext) {
|
||||
features.onActivityCreate(activity)
|
||||
inAppOverlay.onActivityCreate(activity)
|
||||
scriptRuntime.eachModule { callFunction("module.onSnapMainActivityCreate", activity) }
|
||||
actionManager.onActivityCreate()
|
||||
|
||||
val isTestModeEnabled = appContext.bridgeClient.getDebugProp("test_mode", "false") == "true"
|
||||
if (safeMode && !isTestModeEnabled) {
|
||||
appContext.inAppOverlay.showStatusToast(
|
||||
Icons.Outlined.Cancel,
|
||||
"Failed to load security features! Snapchat may not work properly.",
|
||||
durationMs = 3000
|
||||
)
|
||||
}
|
||||
}
|
||||
}.also { time ->
|
||||
appContext.log.verbose("onActivityCreate took $time")
|
||||
@@ -268,7 +255,6 @@ class PurrfectSnap {
|
||||
}
|
||||
|
||||
val lateInit = appContext.native.initOnce {
|
||||
verifyKey(BuildConfig.NATIVE_KEY)
|
||||
nativeUnaryCallCallback = { request ->
|
||||
appContext.event.post(NativeUnaryCallEvent(request.uri, request.buffer)) {
|
||||
request.buffer = buffer
|
||||
@@ -453,10 +439,26 @@ class PurrfectSnap {
|
||||
}
|
||||
}
|
||||
val stringResources = strings(androidx.compose.material3.R.string::class, androidx.compose.ui.R.string::class)
|
||||
fun resolveComposeString(key: Int): String? {
|
||||
val name = stringResources[key]?.replaceFirst("m3c_", "") ?: return null
|
||||
return appContext.translation.getOrNull("material3_strings.${name}") ?: ""
|
||||
}
|
||||
|
||||
fun resolveInvalidString(resources: Resources, key: Int): String? {
|
||||
val type = runCatching { resources.getResourceTypeName(key) }.getOrNull() ?: return ""
|
||||
return if (type == "string") null else ""
|
||||
}
|
||||
|
||||
Resources::class.java.getMethod("getString", Int::class.javaPrimitiveType).hook(HookStage.BEFORE) { param ->
|
||||
val key = param.arg<Int>(0)
|
||||
val name = stringResources[key]?.replaceFirst("m3c_", "") ?: return@hook
|
||||
param.setResult(appContext.translation.getOrNull("material3_strings.${name}") ?: "")
|
||||
resolveComposeString(key)?.let { param.setResult(it); return@hook }
|
||||
resolveInvalidString(param.thisObject() as Resources, key)?.let { param.setResult(it) }
|
||||
}
|
||||
|
||||
Resources::class.java.getMethod("getText", Int::class.javaPrimitiveType).hook(HookStage.BEFORE) { param ->
|
||||
val key = param.arg<Int>(0)
|
||||
resolveComposeString(key)?.let { param.setResult(it); return@hook }
|
||||
resolveInvalidString(param.thisObject() as Resources, key)?.let { param.setResult(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.WarningAmber
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -36,6 +38,7 @@ 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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -144,13 +147,20 @@ class BulkMessagingAction : AbstractAction() {
|
||||
ViewAppearanceHelper.newAlertDialogBuilder(ctx)
|
||||
.setTitle("...")
|
||||
.setView(LinearLayout(ctx).apply {
|
||||
val padding = (16 * ctx.resources.displayMetrics.density).toInt()
|
||||
val spacing = (8 * ctx.resources.displayMetrics.density).toInt()
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
setPadding(padding, padding, padding, padding)
|
||||
addView(statusTextView.apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
setSingleLine(false)
|
||||
setPadding(0, 0, 0, spacing)
|
||||
})
|
||||
addView(ProgressBar(ctx).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
})
|
||||
addView(ProgressBar(ctx))
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
@@ -184,21 +194,71 @@ class BulkMessagingAction : AbstractAction() {
|
||||
onConfirm: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onCancel,
|
||||
title = { Text(text = translation["confirmation_dialog.title"]) },
|
||||
text = { Text(text = translation["confirmation_dialog.message"]) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(text = context.translation["button.positive"])
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(text = context.translation["button.negative"])
|
||||
Dialog(onDismissRequest = onCancel) {
|
||||
val shape = RoundedCornerShape(22.dp)
|
||||
Surface(
|
||||
shape = shape,
|
||||
color = Color.Transparent,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 16.dp,
|
||||
border = BorderStroke(1.dp, BulkMessagingPalette.glowStroke)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(BulkMessagingPalette.cardOverlay, shape)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = BulkMessagingPalette.faintSurface
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.WarningAmber,
|
||||
contentDescription = null,
|
||||
tint = BulkMessagingPalette.textPrimary,
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = translation["confirmation_dialog.title"],
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = BulkMessagingPalette.textPrimary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
text = translation["confirmation_dialog.message"],
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = BulkMessagingPalette.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally)
|
||||
) {
|
||||
Button(
|
||||
onClick = onCancel,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White.copy(alpha = 0.08f),
|
||||
contentColor = BulkMessagingPalette.textPrimary
|
||||
)
|
||||
) {
|
||||
Text(text = context.translation["button.negative"])
|
||||
}
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = BulkMessagingPalette.glowPrimary.copy(alpha = 0.34f),
|
||||
contentColor = BulkMessagingPalette.textPrimary
|
||||
)
|
||||
) {
|
||||
Text(text = context.translation["button.positive"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterFriends(friends: List<FriendInfo>, filter: Filter, nameFilter: String): List<FriendInfo> {
|
||||
@@ -728,7 +788,7 @@ class BulkMessagingAction : AbstractAction() {
|
||||
ConversationType.FRIENDS_ONLY -> translation["no_friends_found"]
|
||||
ConversationType.GROUPS_ONLY -> translation["no_groups_found"]
|
||||
ConversationType.BOTH -> translation["no_friends_or_groups_found"]
|
||||
}, fontSize = 12.sp, fontWeight = FontWeight.Light, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
|
||||
}, fontSize = 12.sp, fontWeight = FontWeight.Light, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = BulkMessagingPalette.textPrimary)
|
||||
}
|
||||
}
|
||||
items(friends, key = { it.userId!! }) { friendInfo ->
|
||||
@@ -1215,7 +1275,12 @@ class BulkMessagingAction : AbstractAction() {
|
||||
) {
|
||||
actionsList.forEach { (textBuilder, actionFunction) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = remember(selectedFriends.size, selectedGroups.size) { textBuilder() }) },
|
||||
text = {
|
||||
Text(
|
||||
text = remember(selectedFriends.size, selectedGroups.size) { textBuilder() },
|
||||
color = BulkMessagingPalette.textPrimary
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
actionsMenuExpanded = false
|
||||
showConfirmationDialog = true
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package me.eternal.purrfectsnap.core.action.impl
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Color as AndroidColor
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -31,6 +34,7 @@ import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
@@ -45,13 +49,23 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import com.github.skydoves.colorpicker.compose.BrightnessSlider
|
||||
import com.github.skydoves.colorpicker.compose.ColorPickerController
|
||||
import com.github.skydoves.colorpicker.compose.HsvColorPicker
|
||||
import kotlinx.coroutines.*
|
||||
import me.eternal.purrfectsnap.common.data.ContentType
|
||||
import me.eternal.purrfectsnap.common.database.impl.FriendInfo
|
||||
import me.eternal.purrfectsnap.common.database.impl.FriendFeedEntry
|
||||
import me.eternal.purrfectsnap.common.bridge.wrapper.LoggedMessage
|
||||
import me.eternal.purrfectsnap.common.bridge.wrapper.LoggerWrapper
|
||||
import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog
|
||||
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState
|
||||
import me.eternal.purrfectsnap.core.action.AbstractAction
|
||||
@@ -60,14 +74,21 @@ import me.eternal.purrfectsnap.core.logger.CoreLogger
|
||||
import me.eternal.purrfectsnap.core.messaging.ConversationExporter
|
||||
import me.eternal.purrfectsnap.core.messaging.ExportFormat
|
||||
import me.eternal.purrfectsnap.core.messaging.ExportParams
|
||||
import me.eternal.purrfectsnap.core.ui.ViewAppearanceHelper
|
||||
import me.eternal.purrfectsnap.core.wrapper.impl.Message
|
||||
import java.io.File
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
private data class ExportColorParticipant(
|
||||
val userId: String,
|
||||
val displayName: String,
|
||||
val username: String
|
||||
)
|
||||
|
||||
class ExportChatMessages : AbstractAction() {
|
||||
private val translation by lazy { context.translation.getCategory("chat_export") }
|
||||
private val dialogLogs = mutableListOf<String>()
|
||||
private var dialogTitle by mutableStateOf("")
|
||||
private var dialogText by mutableStateOf("")
|
||||
private var currentActionDialog: AlertDialog? = null
|
||||
private val dialogBackground = Brush.verticalGradient(
|
||||
listOf(
|
||||
@@ -88,18 +109,59 @@ class ExportChatMessages : AbstractAction() {
|
||||
)
|
||||
)
|
||||
|
||||
private data class ExportTarget(
|
||||
val outputFile: File,
|
||||
val finalize: (File) -> String
|
||||
)
|
||||
|
||||
private fun resolveExportTarget(fileName: String, mimeType: String): ExportTarget {
|
||||
val configuredFolder = context.config.downloader.saveFolder.get()?.trim().orEmpty()
|
||||
val defaultTarget = {
|
||||
val publicFolder = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"PurrfectSnap"
|
||||
).also { if (!it.exists()) it.mkdirs() }
|
||||
val outputFile = publicFolder.resolve(fileName).also { if (it.exists()) it.delete() }
|
||||
ExportTarget(outputFile) { file -> file.absolutePath }
|
||||
}
|
||||
|
||||
if (configuredFolder.isBlank()) {
|
||||
return defaultTarget()
|
||||
}
|
||||
|
||||
val outputFolder = runCatching {
|
||||
DocumentFile.fromTreeUri(context.androidContext, Uri.parse(configuredFolder))
|
||||
}.getOrNull()
|
||||
|
||||
if (outputFolder == null || !outputFolder.canWrite()) {
|
||||
return defaultTarget()
|
||||
}
|
||||
|
||||
val tempFile = File(context.androidContext.cacheDir, fileName).also {
|
||||
if (it.exists()) it.delete()
|
||||
}
|
||||
return ExportTarget(tempFile) { file ->
|
||||
val outputFile = outputFolder.createFile(mimeType, fileName)
|
||||
?: throw IllegalStateException("Failed to create export file")
|
||||
context.androidContext.contentResolver.openOutputStream(outputFile.uri)?.use { out ->
|
||||
file.inputStream().use { it.copyTo(out) }
|
||||
} ?: throw IllegalStateException("Failed to write export file")
|
||||
outputFile.uri.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun logDialog(message: String) {
|
||||
context.runOnUiThread {
|
||||
if (dialogLogs.size > 10) dialogLogs.removeAt(0)
|
||||
dialogLogs.add(message)
|
||||
context.log.debug("dialog: $message", "ExportChatMessages")
|
||||
currentActionDialog!!.setMessage(dialogLogs.joinToString("\n"))
|
||||
dialogText = dialogLogs.joinToString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setStatus(message: String) {
|
||||
context.runOnUiThread {
|
||||
currentActionDialog!!.setTitle(message)
|
||||
dialogTitle = message
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +181,10 @@ class ExportChatMessages : AbstractAction() {
|
||||
var showConversationPicker by remember { mutableStateOf(false) }
|
||||
var showFormatPicker by remember { mutableStateOf(false) }
|
||||
var showMessageTypePicker by remember { mutableStateOf(false) }
|
||||
val colorOverrides = remember { mutableStateMapOf<String, String>() }
|
||||
var colorPickerTarget by remember { mutableStateOf<ExportColorParticipant?>(null) }
|
||||
var colorPickerValue by remember { mutableStateOf<Color?>(null) }
|
||||
var participants by remember { mutableStateOf<List<ExportColorParticipant>>(emptyList()) }
|
||||
val allFriends by rememberAsyncMutableState(null) { context.database.getAllFriends().associateBy { it.userId!! } }
|
||||
val myUserId = context.database.myUserId
|
||||
val focusManager = LocalFocusManager.current
|
||||
@@ -272,6 +338,66 @@ class ExportChatMessages : AbstractAction() {
|
||||
accent = accent
|
||||
)
|
||||
|
||||
SectionLabel("Participant colors")
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 120.dp, max = 260.dp)
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(Color.White.copy(alpha = 0.06f))
|
||||
.border(1.dp, Color.White.copy(alpha = 0.12f), RoundedCornerShape(18.dp))
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
if (participants.isEmpty()) {
|
||||
BasicText(
|
||||
text = "Select conversations to customize participant colors.",
|
||||
style = TextStyle(color = Color(0xFFB1B4D7), fontSize = 12.sp)
|
||||
)
|
||||
} else {
|
||||
participants.forEach { participant ->
|
||||
val colorHex = colorOverrides[participant.userId]
|
||||
val color = colorHex?.let { Color(AndroidColor.parseColor(it)) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(Color.White.copy(alpha = 0.05f))
|
||||
.border(1.dp, Color.White.copy(alpha = 0.12f), RoundedCornerShape(14.dp))
|
||||
.clickable {
|
||||
colorPickerTarget = participant
|
||||
colorPickerValue = color
|
||||
}
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ColorSwatch(color = color)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
BasicText(
|
||||
text = participant.displayName,
|
||||
style = TextStyle(color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.SemiBold),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
BasicText(
|
||||
text = participant.username,
|
||||
style = TextStyle(color = Color(0xFFB1B4D7), fontSize = 12.sp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
BasicText(
|
||||
text = colorHex ?: "Auto",
|
||||
style = TextStyle(color = Color(0xFFB1B4D7), fontSize = 11.sp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
@@ -284,15 +410,17 @@ class ExportChatMessages : AbstractAction() {
|
||||
PrimaryButton(
|
||||
text = translation["dialog_positive_button"],
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = selectedFeedEntries.isNotEmpty(),
|
||||
enabled = selectedFeedEntries.isNotEmpty() || feedEntries.isNotEmpty(),
|
||||
onClick = {
|
||||
val selection = if (selectedFeedEntries.isEmpty()) feedEntries else selectedFeedEntries
|
||||
exportChatForConversations(
|
||||
selectedFeedEntries,
|
||||
selection,
|
||||
ExportParams(
|
||||
exportFormat = exportType,
|
||||
messageTypeFilter = messageTypeFilter.takeIf { it.isNotEmpty() },
|
||||
amountOfMessages = amountOfMessages.takeIf { it != -1 },
|
||||
downloadMedias = downloadMedias
|
||||
downloadMedias = downloadMedias,
|
||||
colorOverrides = colorOverrides.takeIf { it.isNotEmpty() }?.toMap()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -307,6 +435,56 @@ class ExportChatMessages : AbstractAction() {
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedFeedEntries.toList(), allFriends) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val selection = selectedFeedEntries.toList()
|
||||
if (selection.isEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
participants = emptyList()
|
||||
}
|
||||
return@withContext
|
||||
}
|
||||
val participantsMap = linkedMapOf<String, ExportColorParticipant>()
|
||||
selection.forEach { entry ->
|
||||
val userIds = context.database.getConversationParticipants(entry.key!!, useCache = false) ?: emptyList()
|
||||
userIds.forEach { userId ->
|
||||
if (participantsMap.containsKey(userId)) return@forEach
|
||||
val friend = allFriends?.get(userId) ?: context.database.getFriendInfo(userId)
|
||||
val displayName = friend?.displayName ?: friend?.mutableUsername ?: userId
|
||||
val username = friend?.mutableUsername ?: userId
|
||||
participantsMap[userId] = ExportColorParticipant(userId, displayName, username)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
participants = participantsMap.values.toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
colorPickerTarget?.let { target ->
|
||||
Dialog(
|
||||
onDismissRequest = { colorPickerTarget = null },
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
ExportColorPickerDialog(
|
||||
participant = target,
|
||||
initialColor = colorPickerValue,
|
||||
onSave = { color ->
|
||||
if (color == null) {
|
||||
colorOverrides.remove(target.userId)
|
||||
} else {
|
||||
colorOverrides[target.userId] = colorToHex(color)
|
||||
}
|
||||
colorPickerTarget = null
|
||||
},
|
||||
onClear = {
|
||||
colorOverrides.remove(target.userId)
|
||||
colorPickerTarget = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showConversationPicker) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -340,6 +518,31 @@ class ExportChatMessages : AbstractAction() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (feedEntries.isNotEmpty()) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
SecondaryButton(
|
||||
text = t("text_field_selection_all"),
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
selectedFeedEntries.clear()
|
||||
selectedFeedEntries.addAll(feedEntries)
|
||||
}
|
||||
)
|
||||
SecondaryButton(
|
||||
text = translation["dialog_negative_button"],
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { selectedFeedEntries.clear() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -686,6 +889,190 @@ class ExportChatMessages : AbstractAction() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExportProgressDialog(
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(dialogBackground)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(1.2.dp, accentGradient, RoundedCornerShape(26.dp)),
|
||||
shape = RoundedCornerShape(26.dp),
|
||||
tonalElevation = 0.dp,
|
||||
color = Color.White.copy(alpha = 0.04f)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(panelOverlay)
|
||||
.padding(18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
Text(
|
||||
text = dialogTitle,
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 120.dp, max = 280.dp)
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(Color.White.copy(alpha = 0.06f))
|
||||
.border(1.dp, Color.White.copy(alpha = 0.12f), RoundedCornerShape(18.dp))
|
||||
.verticalScroll(scrollState)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
BasicText(
|
||||
text = dialogText,
|
||||
style = TextStyle(color = Color(0xFFD9D3FF), fontSize = 12.sp)
|
||||
)
|
||||
}
|
||||
SecondaryButton(
|
||||
text = translation["dialog_negative_button"],
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onCancel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColorSwatch(color: Color?) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(26.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(color ?: Color.White.copy(alpha = 0.1f))
|
||||
.border(1.dp, Color.White.copy(alpha = 0.3f), RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (color == null) {
|
||||
BasicText(
|
||||
text = "A",
|
||||
style = TextStyle(color = Color.White, fontSize = 10.sp, fontWeight = FontWeight.SemiBold)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorToHex(color: Color): String {
|
||||
return String.format("#%06X", 0xFFFFFF and color.toArgb())
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExportColorPickerDialog(
|
||||
participant: ExportColorParticipant,
|
||||
initialColor: Color?,
|
||||
onSave: (Color?) -> Unit,
|
||||
onClear: () -> Unit
|
||||
) {
|
||||
var currentColor by remember { mutableStateOf(initialColor ?: Color.White) }
|
||||
val controller = remember { ColorPickerController().apply { selectByColor(currentColor, false) } }
|
||||
var colorHexValue by remember { mutableStateOf(colorToHex(currentColor).removePrefix("#")) }
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 18.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = Color.White.copy(alpha = 0.06f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 16.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
Color(0xFF8C7BFF).copy(alpha = 0.6f),
|
||||
Color(0xFF5FD8FF).copy(alpha = 0.5f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFF1B1636))
|
||||
.padding(18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Color for ${participant.displayName}",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
)
|
||||
TextField(
|
||||
value = colorHexValue,
|
||||
onValueChange = { value ->
|
||||
colorHexValue = value
|
||||
runCatching {
|
||||
val parsed = Color(AndroidColor.parseColor("#$value"))
|
||||
currentColor = parsed
|
||||
controller.selectByColor(parsed, true)
|
||||
}
|
||||
},
|
||||
label = { Text(text = "Hex Color") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.White.copy(alpha = 0.08f),
|
||||
unfocusedContainerColor = Color.White.copy(alpha = 0.05f),
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
cursorColor = Color(0xFF8EF0F3),
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White
|
||||
)
|
||||
)
|
||||
HsvColorPicker(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(240.dp),
|
||||
controller = controller,
|
||||
onColorChanged = {
|
||||
if (!it.fromUser) return@HsvColorPicker
|
||||
currentColor = it.color
|
||||
colorHexValue = colorToHex(it.color).removePrefix("#")
|
||||
}
|
||||
)
|
||||
BrightnessSlider(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(30.dp),
|
||||
controller = controller
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
SecondaryButton(
|
||||
text = "Auto",
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onClear
|
||||
)
|
||||
PrimaryButton(
|
||||
text = "Save",
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { onSave(currentColor) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
context.coroutineScope.launch(Dispatchers.Main) {
|
||||
createComposeAlertDialog(context.mainActivity!!) { alertDialog ->
|
||||
@@ -722,18 +1109,14 @@ class ExportChatMessages : AbstractAction() {
|
||||
) {
|
||||
dialogLogs.clear()
|
||||
val jobs = mutableListOf<Job>()
|
||||
dialogTitle = translation["exporting_chats"]
|
||||
dialogText = ""
|
||||
|
||||
currentActionDialog = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
|
||||
.setTitle(translation["exporting_chats"])
|
||||
.setCancelable(false)
|
||||
.setMessage("")
|
||||
.create()
|
||||
|
||||
val conversationSize = translation.format("processing_chats", "amount" to conversations.size.toString())
|
||||
|
||||
logDialog(conversationSize)
|
||||
|
||||
context.coroutineScope.launch {
|
||||
val exportJob = context.coroutineScope.launch {
|
||||
conversations.forEach { conversation ->
|
||||
launch {
|
||||
runCatching {
|
||||
@@ -747,17 +1130,33 @@ class ExportChatMessages : AbstractAction() {
|
||||
}
|
||||
jobs.joinAll()
|
||||
logDialog(translation["finished"])
|
||||
}.also {
|
||||
currentActionDialog?.setButton(DialogInterface.BUTTON_POSITIVE, translation["dialog_negative_button"]) { dialog, _ ->
|
||||
it.cancel()
|
||||
jobs.forEach { it.cancel() }
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
currentActionDialog!!.also {
|
||||
it.setCanceledOnTouchOutside(false)
|
||||
}.show()
|
||||
currentActionDialog = createComposeAlertDialog(context.mainActivity!!) { alertDialog ->
|
||||
ExportProgressDialog {
|
||||
exportJob.cancel()
|
||||
jobs.forEach { it.cancel() }
|
||||
alertDialog.dismiss()
|
||||
}
|
||||
}.apply {
|
||||
setCanceledOnTouchOutside(false)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchLoggerMessages(conversationId: String): List<LoggedMessage> {
|
||||
return runCatching {
|
||||
val loggerWrapper = LoggerWrapper(context.androidContext)
|
||||
val messages = mutableListOf<LoggedMessage>()
|
||||
var fromTimestamp = Long.MAX_VALUE
|
||||
while (true) {
|
||||
val batch = loggerWrapper.fetchMessages(conversationId, fromTimestamp, 500, reverseOrder = true)
|
||||
if (batch.isEmpty()) break
|
||||
messages.addAll(batch)
|
||||
fromTimestamp = batch.last().sendTimestamp
|
||||
}
|
||||
messages
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
private suspend fun exportFullConversation(
|
||||
@@ -771,35 +1170,59 @@ class ExportChatMessages : AbstractAction() {
|
||||
context.database.getFriendInfo(it)
|
||||
}?.associateBy { it.userId!! } ?: emptyMap()
|
||||
|
||||
val loggerMessages = fetchLoggerMessages(conversationId)
|
||||
val participantMap = conversationParticipants.toMutableMap().apply {
|
||||
loggerMessages.forEach { message ->
|
||||
if (containsKey(message.userId)) return@forEach
|
||||
this[message.userId] = FriendInfo(
|
||||
userId = message.userId,
|
||||
displayName = message.username,
|
||||
username = message.username,
|
||||
usernameForSorting = message.username
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val conversationName = feedEntry.feedDisplayName ?: conversationParticipants.values.take(3).joinToString("_") { it.mutableUsername ?: "" }
|
||||
|
||||
val publicFolder = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "PurrfectSnap").also { if (!it.exists()) it.mkdirs() }
|
||||
val outputFile = publicFolder.resolve("conversation_${conversationName}_${System.currentTimeMillis()}.${exportParams.exportFormat.extension}")
|
||||
val outputName = "conversation_${conversationName}_${System.currentTimeMillis()}.${exportParams.exportFormat.extension}"
|
||||
val mimeType = when (exportParams.exportFormat) {
|
||||
ExportFormat.JSON -> "application/json"
|
||||
ExportFormat.TEXT -> "text/plain"
|
||||
ExportFormat.HTML -> "text/html"
|
||||
}
|
||||
val outputTarget = resolveExportTarget(outputName, mimeType)
|
||||
val outputFile = outputTarget.outputFile
|
||||
|
||||
logDialog(translation.format("exporting_message", "conversation" to conversationName))
|
||||
|
||||
val conversationExporter = ConversationExporter(
|
||||
context = context,
|
||||
friendFeedEntry = feedEntry,
|
||||
conversationParticipants = conversationParticipants,
|
||||
conversationParticipants = participantMap,
|
||||
exportParams = exportParams,
|
||||
cacheFolder = publicFolder.resolve("cache").also { if (!it.exists()) it.mkdirs() },
|
||||
cacheFolder = context.androidContext.cacheDir.resolve("chat_export").also { if (!it.exists()) it.mkdirs() },
|
||||
outputFile = outputFile,
|
||||
).apply { init(); printLog = {
|
||||
logDialog(it.toString())
|
||||
} }
|
||||
|
||||
var foundMessageCount = 0
|
||||
val exportedOrderKeys = mutableSetOf<Long>()
|
||||
|
||||
var lastMessageId = fetchMessagesPaginated(conversationId, Long.MAX_VALUE, amount = 1).firstOrNull()?.also {
|
||||
conversationExporter.readMessage(it)
|
||||
var lastMessageId: Long? = null
|
||||
fetchMessagesPaginated(conversationId, Long.MAX_VALUE, amount = 1).firstOrNull()?.also { message ->
|
||||
conversationExporter.readMessage(message)
|
||||
foundMessageCount++
|
||||
}?.messageDescriptor?.messageId ?: run {
|
||||
logDialog(translation["no_messages_found"])
|
||||
return
|
||||
message.orderKey?.let { exportedOrderKeys.add(it) }
|
||||
lastMessageId = message.messageDescriptor?.messageId
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (lastMessageId == null) {
|
||||
logDialog(translation["no_messages_found"])
|
||||
}
|
||||
|
||||
while (lastMessageId != null) {
|
||||
val fetchedMessages = fetchMessagesPaginated(conversationId, lastMessageId, amount = 500).toMutableList()
|
||||
if (fetchedMessages.isEmpty()) break
|
||||
|
||||
@@ -813,28 +1236,49 @@ class ExportChatMessages : AbstractAction() {
|
||||
}
|
||||
}
|
||||
|
||||
foundMessageCount += fetchedMessages.size
|
||||
val remainingLimit = exportParams.amountOfMessages?.let { it - foundMessageCount } ?: Int.MAX_VALUE
|
||||
if (remainingLimit <= 0) break
|
||||
|
||||
if (exportParams.amountOfMessages != null && foundMessageCount >= exportParams.amountOfMessages) {
|
||||
fetchedMessages.reversed().subList(0, exportParams.amountOfMessages - (foundMessageCount - fetchedMessages.size)).forEach { message ->
|
||||
conversationExporter.readMessage(message)
|
||||
}
|
||||
break
|
||||
val messagesToWrite = fetchedMessages.reversed().let { messages ->
|
||||
if (messages.size <= remainingLimit) messages else messages.subList(0, remainingLimit)
|
||||
}
|
||||
|
||||
fetchedMessages.reversed().forEach { message ->
|
||||
messagesToWrite.forEach { message ->
|
||||
conversationExporter.readMessage(message)
|
||||
foundMessageCount++
|
||||
message.orderKey?.let { exportedOrderKeys.add(it) }
|
||||
}
|
||||
|
||||
setStatus("Exporting (found ${foundMessageCount})")
|
||||
}
|
||||
|
||||
if (loggerMessages.isNotEmpty() && (exportParams.amountOfMessages == null || foundMessageCount < exportParams.amountOfMessages)) {
|
||||
val parsedLoggerMessages = loggerMessages.mapNotNull { conversationExporter.parseLoggedMessage(it) }
|
||||
for (loggedMessage in parsedLoggerMessages.asReversed()) {
|
||||
if (exportedOrderKeys.contains(loggedMessage.orderKey)) continue
|
||||
val filter = exportParams.messageTypeFilter
|
||||
if (filter != null && !filter.contains(loggedMessage.contentType)) continue
|
||||
if (exportParams.amountOfMessages != null && foundMessageCount >= exportParams.amountOfMessages) break
|
||||
conversationExporter.readLoggedMessage(loggedMessage)
|
||||
foundMessageCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (exportParams.exportFormat == ExportFormat.HTML) conversationExporter.awaitDownload()
|
||||
conversationExporter.close()
|
||||
logDialog(translation["writing_output"])
|
||||
dialogLogs.clear()
|
||||
val exportedPath = runCatching { outputTarget.finalize(outputFile) }.getOrElse { error ->
|
||||
logDialog("Failed to write export output")
|
||||
logDialog(error.toString())
|
||||
context.log.error("Failed to finalize chat export", error)
|
||||
return
|
||||
}
|
||||
if (outputFile.parentFile == context.androidContext.cacheDir) {
|
||||
outputFile.delete()
|
||||
}
|
||||
logDialog("\n" + translation.format("exported_to",
|
||||
"path" to outputFile.absolutePath.toString()
|
||||
"path" to exportedPath
|
||||
) + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package me.eternal.purrfectsnap.core.action.impl
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteDatabase.OpenParams
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
@@ -100,6 +102,49 @@ class ExportMemories : AbstractAction() {
|
||||
get() = storyTitle.replace(Regex("[^a-zA-Z0-9\\s]"), "").trim().replace(Regex("\\s+"), "_")
|
||||
}
|
||||
|
||||
private data class ExportTarget(
|
||||
val outputFile: File,
|
||||
val finalize: (File) -> String
|
||||
)
|
||||
|
||||
private fun resolveExportTarget(fileName: String, mimeType: String): ExportTarget {
|
||||
val configuredFolder = context.config.downloader.saveFolder.get()?.trim().orEmpty()
|
||||
val defaultTarget = {
|
||||
val documentsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
|
||||
val outputDir = documentsDir.takeIf { it.exists() || it.mkdirs() }
|
||||
?: context.androidContext.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
|
||||
?: context.androidContext.filesDir
|
||||
val outputFile = File(outputDir, fileName).also {
|
||||
if (it.exists()) it.delete()
|
||||
}
|
||||
ExportTarget(outputFile) { file -> file.absolutePath }
|
||||
}
|
||||
|
||||
if (configuredFolder.isBlank()) {
|
||||
return defaultTarget()
|
||||
}
|
||||
|
||||
val outputFolder = runCatching {
|
||||
DocumentFile.fromTreeUri(context.androidContext, Uri.parse(configuredFolder))
|
||||
}.getOrNull()
|
||||
|
||||
if (outputFolder == null || !outputFolder.canWrite()) {
|
||||
return defaultTarget()
|
||||
}
|
||||
|
||||
val tempFile = File(context.androidContext.cacheDir, fileName).also {
|
||||
if (it.exists()) it.delete()
|
||||
}
|
||||
return ExportTarget(tempFile) { file ->
|
||||
val outputFile = outputFolder.createFile(mimeType, fileName)
|
||||
?: throw IllegalStateException("Failed to create export file")
|
||||
context.androidContext.contentResolver.openOutputStream(outputFile.uri)?.use { out ->
|
||||
file.inputStream().use { it.copyTo(out) }
|
||||
} ?: throw IllegalStateException("Failed to write export file")
|
||||
outputFile.uri.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalEncodingApi::class)
|
||||
private suspend fun exportMemories(
|
||||
scope: CoroutineScope = context.coroutineScope,
|
||||
@@ -111,9 +156,11 @@ class ExportMemories : AbstractAction() {
|
||||
) {
|
||||
val downloadContext = Dispatchers.IO.limitedParallelism(10)
|
||||
val writeToZipContext = Dispatchers.IO.limitedParallelism(1)
|
||||
val outputZip = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "memories_" + System.currentTimeMillis() + ".zip").also {
|
||||
if (it.exists()) it.delete()
|
||||
}
|
||||
val outputTarget = resolveExportTarget(
|
||||
"memories_${System.currentTimeMillis()}.zip",
|
||||
"application/zip"
|
||||
)
|
||||
val outputZip = outputTarget.outputFile
|
||||
val okHttpClient = OkHttpClient.Builder().build()
|
||||
val outputZipFile = withContext(Dispatchers.IO) {
|
||||
ZipOutputStream(FileOutputStream(outputZip)).apply {
|
||||
@@ -249,7 +296,16 @@ class ExportMemories : AbstractAction() {
|
||||
withContext(Dispatchers.IO) {
|
||||
outputZipFile.close()
|
||||
}
|
||||
context.longToast("Exported to ${outputZip.absolutePath}")
|
||||
val exportedPath = runCatching { outputTarget.finalize(outputZip) }
|
||||
.getOrElse { error ->
|
||||
context.log.error("Failed to finalize memories export", error)
|
||||
context.longToast("Failed to export memories")
|
||||
return
|
||||
}
|
||||
if (outputZip.parentFile == context.androidContext.cacheDir) {
|
||||
outputZip.delete()
|
||||
}
|
||||
context.longToast("Exported to $exportedPath")
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package me.eternal.purrfectsnap.core.action.impl
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
@@ -12,9 +14,11 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.People
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -26,25 +30,48 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import me.eternal.purrfectsnap.common.data.FriendLinkType
|
||||
import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog
|
||||
import me.eternal.purrfectsnap.core.action.AbstractAction
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.ActivityResultEvent
|
||||
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie
|
||||
import me.eternal.purrfectsnap.common.util.snap.RemoteMediaResolver
|
||||
import me.eternal.purrfectsnap.core.features.impl.experiments.AddFriendSourceSpoof
|
||||
import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging
|
||||
import me.eternal.purrfectsnap.core.wrapper.impl.Snapchatter
|
||||
import kotlin.random.Random
|
||||
|
||||
class ManageFriendList : AbstractAction() {
|
||||
companion object {
|
||||
private var openSuggestedOnLaunch = false
|
||||
|
||||
@Synchronized
|
||||
fun requestOpenSuggestedOnLaunch() {
|
||||
openSuggestedOnLaunch = true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun consumeOpenSuggestedOnLaunch(): Boolean {
|
||||
val shouldOpen = openSuggestedOnLaunch
|
||||
openSuggestedOnLaunch = false
|
||||
return shouldOpen
|
||||
}
|
||||
}
|
||||
|
||||
private val translation by lazy { context.translation.getCategory("friend_list") }
|
||||
private val dialogBackground = Brush.verticalGradient(
|
||||
listOf(
|
||||
@@ -216,6 +243,22 @@ class ManageFriendList : AbstractAction() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSuggestedFriends(
|
||||
coroutineScope: CoroutineScope,
|
||||
onLoaded: (List<String>) -> Unit
|
||||
) {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
val blacklist = getUserIdBlacklist()
|
||||
val suggestedFriends = context.database.getAllFriends()
|
||||
.filter { it.userId !in blacklist && it.friendLinkType == FriendLinkType.SUGGESTED.value }
|
||||
.sortedByDescending { it.addedTimestamp }
|
||||
.mapNotNull { it.userId }
|
||||
withContext(Dispatchers.Main) {
|
||||
onLoaded(suggestedFriends)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreate() {
|
||||
context.event.subscribe(ActivityResultEvent::class) { event ->
|
||||
if (event.requestCode == pendingPickerAction?.first) {
|
||||
@@ -254,12 +297,27 @@ class ManageFriendList : AbstractAction() {
|
||||
}
|
||||
|
||||
private val userIdToSnapchatter = mutableMapOf<String, Snapchatter>()
|
||||
|
||||
private fun getUserIdBlacklist() = arrayOf(
|
||||
context.database.myUserId,
|
||||
"b42f1f70-5a8b-4c53-8c25-34e7ec9e6781",
|
||||
"84ee8839-3911-492d-8b94-72dd80f3713a",
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun ManagerDialog() {
|
||||
val pendingFriendRequests = remember { mutableStateMapOf<String, Job>() }
|
||||
var fetchedFriends by remember { mutableStateOf<List<String>?>(null) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val openSuggestedOnLaunch = remember { consumeOpenSuggestedOnLaunch() }
|
||||
val bitmojiCache = remember { me.eternal.purrfectsnap.core.util.EvictingMap<String, Bitmap>(50) }
|
||||
val noBitmojiBitmap = remember { BitmapFactory.decodeResource(context.resources, android.R.drawable.ic_menu_report_image).asImageBitmap() }
|
||||
|
||||
LaunchedEffect(openSuggestedOnLaunch) {
|
||||
if (openSuggestedOnLaunch) {
|
||||
loadSuggestedFriends(coroutineScope) { fetchedFriends = it }
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -387,8 +445,30 @@ class ManageFriendList : AbstractAction() {
|
||||
)
|
||||
}
|
||||
}
|
||||
PrimaryButton(
|
||||
text = "Load Suggested Friends",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
loadSuggestedFriends(coroutineScope) { fetchedFriends = it }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
val filteredFriends = remember(fetchedFriends, searchQuery) {
|
||||
val friends = fetchedFriends ?: emptyList()
|
||||
if (searchQuery.isBlank()) {
|
||||
friends.sortedByDescending { context.database.getFriendInfo(it)?.addedTimestamp ?: 0L }
|
||||
} else {
|
||||
friends.filter { userId ->
|
||||
val friendInfo = context.database.getFriendInfo(userId)
|
||||
friendInfo?.mutableUsername?.contains(searchQuery, ignoreCase = true) == true ||
|
||||
friendInfo?.displayName?.contains(searchQuery, ignoreCase = true) == true ||
|
||||
userId.contains(searchQuery, ignoreCase = true)
|
||||
}.sortedByDescending { context.database.getFriendInfo(it)?.addedTimestamp ?: 0L }
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -411,9 +491,10 @@ class ManageFriendList : AbstractAction() {
|
||||
.clickable { fetchedFriends = null },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
||||
contentDescription = context.translation["common.back"],
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
@@ -427,16 +508,45 @@ class ManageFriendList : AbstractAction() {
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
Text(
|
||||
text = translation.get("export_description"),
|
||||
color = Color(0xFFD9D3FF),
|
||||
fontSize = 12.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.size(46.dp))
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color.White.copy(alpha = 0.06f))
|
||||
.border(1.dp, Color.White.copy(alpha = 0.12f), RoundedCornerShape(12.dp))
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
singleLine = true,
|
||||
textStyle = androidx.compose.ui.text.TextStyle(color = Color.White, fontSize = 14.sp),
|
||||
cursorBrush = SolidColor(Color(0xFF8EF0F3)),
|
||||
decorationBox = { innerTextField ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
tint = Color(0xFFB1B4D7),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Box(Modifier.weight(1f)) {
|
||||
if (searchQuery.isEmpty()) {
|
||||
BasicText(
|
||||
"Search...",
|
||||
style = androidx.compose.ui.text.TextStyle(color = Color(0xFFB1B4D7), fontSize = 14.sp)
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
@@ -453,7 +563,7 @@ class ManageFriendList : AbstractAction() {
|
||||
.padding(12.dp)
|
||||
) {
|
||||
item {
|
||||
if (fetchedFriends?.isEmpty() == true) {
|
||||
if (filteredFriends.isEmpty()) {
|
||||
BasicText(
|
||||
context.translation["common.no_friends_found"],
|
||||
style = androidx.compose.ui.text.TextStyle(color = Color(0xFFA8B5D1), fontSize = 13.sp),
|
||||
@@ -461,47 +571,70 @@ class ManageFriendList : AbstractAction() {
|
||||
)
|
||||
}
|
||||
}
|
||||
items(fetchedFriends ?: emptyList()) { userId ->
|
||||
fun fetchLocalLinkType(): FriendLinkType? {
|
||||
return context.database.getFriendInfo(userId)?.friendLinkType?.let { FriendLinkType.fromValue(it) }
|
||||
items(filteredFriends) { userId ->
|
||||
val friendInfo = remember(userId) { context.database.getFriendInfo(userId) }
|
||||
val linkType = remember(friendInfo) {
|
||||
friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }
|
||||
}
|
||||
|
||||
fun isActuallyAdded(): Boolean {
|
||||
val friendInfo = context.database.getFriendInfo(userId)
|
||||
return friendInfo != null &&
|
||||
(friendInfo.friendLinkType == FriendLinkType.MUTUAL.value ||
|
||||
friendInfo.friendLinkType == FriendLinkType.OUTGOING.value) &&
|
||||
friendInfo.addedTimestamp > 0L
|
||||
val isActuallyAdded = remember(friendInfo, linkType) {
|
||||
friendInfo != null && linkType != null && friendInfo.addedTimestamp > 0 &&
|
||||
(linkType == FriendLinkType.MUTUAL || linkType == FriendLinkType.OUTGOING)
|
||||
}
|
||||
|
||||
var friendSnapchatter by remember(userId) { mutableStateOf<Snapchatter?>(null) }
|
||||
var failedToFetch by remember(userId) { mutableStateOf(false) }
|
||||
var friendLinkType by remember(userId) { mutableStateOf(fetchLocalLinkType()) }
|
||||
var actuallyAdded by remember(userId) { mutableStateOf(isActuallyAdded()) }
|
||||
var friendLinkType by remember(userId) { mutableStateOf(linkType) }
|
||||
var actuallyAdded by remember(userId) { mutableStateOf(isActuallyAdded) }
|
||||
|
||||
var bitmojiBitmap by remember(userId, friendInfo?.bitmojiAvatarId) {
|
||||
mutableStateOf(friendInfo?.bitmojiAvatarId?.let { bitmojiCache[it] })
|
||||
}
|
||||
|
||||
LaunchedEffect(userId) {
|
||||
launch(Dispatchers.IO) {
|
||||
friendSnapchatter = userIdToSnapchatter.getOrPut(userId) {
|
||||
context.feature(Messaging::class).fetchSnapchatterInfos(listOf(userId)).firstOrNull() ?: run {
|
||||
failedToFetch = true
|
||||
return@launch
|
||||
if (friendSnapchatter == null && !userIdToSnapchatter.containsKey(userId)) {
|
||||
withContext(Dispatchers.IO) {
|
||||
context.feature(Messaging::class).fetchSnapchatterInfos(listOf(userId)).firstOrNull()?.let {
|
||||
userIdToSnapchatter[userId] = it
|
||||
friendSnapchatter = it
|
||||
}
|
||||
}
|
||||
} else {
|
||||
friendSnapchatter = userIdToSnapchatter[userId]
|
||||
}
|
||||
|
||||
// Polling loop to keep status in sync (like FriendList.kt)
|
||||
while (true) {
|
||||
delay(2000)
|
||||
val newLinkType = fetchLocalLinkType()
|
||||
val newLinkType = friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }
|
||||
if (newLinkType != friendLinkType) {
|
||||
friendLinkType = newLinkType
|
||||
}
|
||||
val newActuallyAdded = isActuallyAdded()
|
||||
val newActuallyAdded = friendInfo != null && linkType != null && friendInfo.addedTimestamp > 0 &&
|
||||
(linkType == FriendLinkType.MUTUAL || linkType == FriendLinkType.OUTGOING)
|
||||
if (newActuallyAdded != actuallyAdded) {
|
||||
actuallyAdded = newActuallyAdded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(userId, friendInfo?.bitmojiAvatarId, friendInfo?.bitmojiSelfieId) {
|
||||
if (bitmojiBitmap != null || friendInfo?.bitmojiAvatarId == null || friendInfo?.bitmojiSelfieId == null) return@LaunchedEffect
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie(
|
||||
friendInfo.bitmojiSelfieId,
|
||||
friendInfo.bitmojiAvatarId,
|
||||
BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D
|
||||
) ?: return@withContext
|
||||
|
||||
runCatching {
|
||||
RemoteMediaResolver.downloadMedia(bitmojiUrl) { inputStream, length ->
|
||||
val avatarId = friendInfo.bitmojiAvatarId ?: return@downloadMedia
|
||||
bitmojiCache[avatarId] = BitmapFactory.decodeStream(inputStream).also {
|
||||
bitmojiBitmap = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -511,94 +644,84 @@ class ManageFriendList : AbstractAction() {
|
||||
.background(Color.White.copy(alpha = 0.05f))
|
||||
.border(1.dp, accentGradient, RoundedCornerShape(14.dp))
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Image(
|
||||
bitmap = remember(bitmojiBitmap) { bitmojiBitmap?.asImageBitmap() ?: noBitmojiBitmap },
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(35.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
BasicText(
|
||||
friendSnapchatter?.let { snapchatter ->
|
||||
snapchatter.displayName?.let { "$it (${snapchatter.username}) " } ?: snapchatter.username ?: context.translation["common.unknown"]
|
||||
} ?: userId,
|
||||
style = androidx.compose.ui.text.TextStyle(color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text(
|
||||
text = friendSnapchatter?.let { snapchatter ->
|
||||
snapchatter.displayName?.let { "$it (${snapchatter.username})" }
|
||||
?: snapchatter.username
|
||||
?: context.translation["common.unknown"]
|
||||
} ?: context.translation["common.unknown"],
|
||||
color = Color.White,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
BasicText(
|
||||
userId,
|
||||
style = androidx.compose.ui.text.TextStyle(color = Color(0xFFB1B4D7), fontSize = 12.sp)
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
friendLinkType?.let { type ->
|
||||
StatusPill(
|
||||
text = type.name.lowercase().replaceFirstChar { it.uppercase() },
|
||||
color = if (type == FriendLinkType.MUTUAL) Color(0xFF8EF0F3) else Color(0xFFD9D3FF)
|
||||
)
|
||||
}
|
||||
if (failedToFetch) {
|
||||
StatusPill(
|
||||
text = translation.get("failed_to_fetch") ?: "Fetch failed",
|
||||
color = Color(0xFFF4B4B4)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (friendSnapchatter != null) {
|
||||
val isPending = pendingFriendRequests.containsKey(userId) && pendingFriendRequests[userId]?.isActive != false
|
||||
val isFollowing = friendLinkType == FriendLinkType.FOLLOWING
|
||||
|
||||
if (friendSnapchatter != null) {
|
||||
val isPending = pendingFriendRequests.containsKey(userId) && pendingFriendRequests[userId]?.isActive != false
|
||||
val isFollowing = friendLinkType == FriendLinkType.FOLLOWING
|
||||
|
||||
PrimaryButton(
|
||||
text = when {
|
||||
isFollowing -> "Following"
|
||||
actuallyAdded -> context.translation["common.added"]
|
||||
isPending -> translation.get("adding") ?: "Adding..."
|
||||
else -> translation.get("add")
|
||||
},
|
||||
modifier = Modifier.widthIn(min = 110.dp),
|
||||
enabled = !actuallyAdded && !isPending && !isFollowing
|
||||
) {
|
||||
if (actuallyAdded || isPending || isFollowing) return@PrimaryButton
|
||||
|
||||
val job = coroutineScope.launch {
|
||||
try {
|
||||
PrimaryButton(
|
||||
text = when {
|
||||
isFollowing -> "Following"
|
||||
actuallyAdded -> context.translation["common.added"]
|
||||
isPending -> translation.get("adding") ?: "Adding..."
|
||||
else -> translation.get("add")
|
||||
},
|
||||
modifier = Modifier.widthIn(min = 110.dp),
|
||||
enabled = !actuallyAdded && !isPending && !isFollowing
|
||||
) {
|
||||
if (actuallyAdded || isPending || isFollowing) return@PrimaryButton
|
||||
|
||||
val prevLinkType = friendLinkType
|
||||
addFriend(userId)
|
||||
delay(300)
|
||||
actuallyAdded = true
|
||||
withTimeout(3000) {
|
||||
var attempts = 0
|
||||
while (attempts < 12) {
|
||||
val currentLinkType = fetchLocalLinkType()
|
||||
if (currentLinkType == FriendLinkType.MUTUAL ||
|
||||
currentLinkType == FriendLinkType.FOLLOWING ||
|
||||
currentLinkType == FriendLinkType.OUTGOING) {
|
||||
friendLinkType = currentLinkType
|
||||
actuallyAdded = true
|
||||
break
|
||||
val job = coroutineScope.launch {
|
||||
withTimeout(10000) {
|
||||
while (friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }?.value == prevLinkType?.value) {
|
||||
delay(500)
|
||||
}
|
||||
attempts++
|
||||
delay(250)
|
||||
}
|
||||
}.apply {
|
||||
invokeOnCompletion {
|
||||
pendingFriendRequests.remove(userId)
|
||||
friendLinkType = friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }
|
||||
actuallyAdded = isActuallyAdded || (friendInfo != null && linkType != null && friendInfo.addedTimestamp > 0 &&
|
||||
(linkType == FriendLinkType.MUTUAL || linkType == FriendLinkType.OUTGOING))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
context.log.error("Failed to add friend or verify status: ${e.message}")
|
||||
actuallyAdded = true
|
||||
pendingFriendRequests[userId] = job
|
||||
}
|
||||
}.apply {
|
||||
invokeOnCompletion {
|
||||
pendingFriendRequests.remove(userId)
|
||||
friendLinkType = fetchLocalLinkType()
|
||||
actuallyAdded = isActuallyAdded() || actuallyAdded
|
||||
if (isPending) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = Color(0xFF8EF0F3)
|
||||
)
|
||||
}
|
||||
}
|
||||
pendingFriendRequests[userId] = job
|
||||
}
|
||||
if (isPending) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = Color(0xFF8EF0F3)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,14 +84,14 @@ class BridgeClient(
|
||||
//ensure the remote process is running
|
||||
runCatching {
|
||||
startActivity(Intent()
|
||||
.setClassName(Constants.SE_PACKAGE_NAME, "me.eternal.purrfectsnap.bridge.ForceStartActivity")
|
||||
.setClassName(Constants.MODULE_PACKAGE_NAME, "me.eternal.purrfectsnap.bridge.ForceStartActivity")
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
)
|
||||
}
|
||||
|
||||
runCatching {
|
||||
val intent = Intent()
|
||||
.setClassName(Constants.SE_PACKAGE_NAME, "me.eternal.purrfectsnap.bridge.BridgeService")
|
||||
.setClassName(Constants.MODULE_PACKAGE_NAME, "me.eternal.purrfectsnap.bridge.BridgeService")
|
||||
runCatching {
|
||||
if (this@BridgeClient::service.isInitialized) {
|
||||
unbindService(this@BridgeClient)
|
||||
@@ -285,3 +285,4 @@ class BridgeClient(
|
||||
|
||||
fun getDebugProp(name: String, defaultValue: String? = null): String? = safeServiceCall { service.getDebugProp(name, defaultValue) }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package me.eternal.purrfectsnap.core.data
|
||||
|
||||
import me.eternal.purrfectsnap.core.util.ClassDetector
|
||||
|
||||
class SnapClassCache (
|
||||
private val classLoader: ClassLoader
|
||||
) {
|
||||
@@ -19,10 +21,50 @@ class SnapClassCache (
|
||||
val conversation by lazy { findClass("com.snapchat.client.messaging.Conversation") }
|
||||
val feedManager by lazy { findClass("com.snapchat.client.messaging.FeedManager\$CppProxy") }
|
||||
val nativeBridge by lazy { runCatching { findClass("com.snapchat.client.valdi.NativeBridge") }.getOrNull() ?: findClass("com.snapchat.client.composer.NativeBridge") }
|
||||
val composerView by lazy { runCatching { findClass("com.snap.composer.views.ComposerView") }.getOrNull() }
|
||||
val composerAction by lazy { runCatching { findClass("com.snap.composer.actions.ComposerAction") }.getOrNull() }
|
||||
val composerFunctionActionAdapter by lazy { runCatching { findClass("com.snap.composer.callable.ComposerFunctionActionAdapter") }.getOrNull() }
|
||||
val valdiView by lazy { runCatching { findClass("com.snap.valdi.views.ValdiView") }.getOrNull() ?: runCatching { findClass("com.snap.composer.views.ComposerView") }.getOrNull() }
|
||||
val valdiFunction by lazy {
|
||||
ClassDetector.findClassBySignature(
|
||||
classLoader = classLoader,
|
||||
knownNames = listOf(
|
||||
"com.snap.valdi.callable.ValdiFunction",
|
||||
"com.snap.composer.callable.ComposerFunction"
|
||||
),
|
||||
methodSignature = { clazz ->
|
||||
clazz.isInterface && clazz.methods.any {
|
||||
it.name == "perform" && it.parameterTypes.size == 1 &&
|
||||
it.returnType == Boolean::class.javaPrimitiveType
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val valdiMarshaller by lazy {
|
||||
ClassDetector.findClassBySignature(
|
||||
classLoader = classLoader,
|
||||
knownNames = listOf(
|
||||
"com.snap.valdi.utils.ValdiMarshaller",
|
||||
"com.snap.composer.utils.ComposerMarshaller"
|
||||
),
|
||||
methodSignature = { clazz ->
|
||||
!clazz.isInterface && clazz.methods.any { it.name == "getUntyped" }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val valdiFunctionActionAdapter by lazy {
|
||||
ClassDetector.findClassBySignature(
|
||||
classLoader = classLoader,
|
||||
knownNames = listOf(
|
||||
"com.snap.valdi.callable.ValdiFunctionActionAdapter",
|
||||
"com.snap.composer.callable.ComposerFunctionActionAdapter"
|
||||
),
|
||||
methodSignature = { clazz ->
|
||||
!clazz.isInterface && clazz.interfaces.isNotEmpty() &&
|
||||
clazz.methods.any { it.name == "perform" && it.parameterTypes.size == 1 }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun findClass(className: String): Class<*> {
|
||||
return try {
|
||||
classLoader.loadClass(className)
|
||||
|
||||
@@ -109,6 +109,7 @@ class FeatureManager(
|
||||
HideStreakRestore(),
|
||||
HideFriendFeedEntry(),
|
||||
RequerySqlite(),
|
||||
RefreshFriendSuggestions(),
|
||||
CallButtonsOverride(),
|
||||
SnapPreview(),
|
||||
BypassScreenshotDetection(),
|
||||
@@ -135,7 +136,7 @@ class FeatureManager(
|
||||
HideActiveMusic(),
|
||||
AutoOpenSnaps(),
|
||||
CustomStreaksExpirationFormat(),
|
||||
ComposerHooks(),
|
||||
ValdiHooks(),
|
||||
DisableCustomTabs(),
|
||||
BestFriendPinning(),
|
||||
ContextMenuFix(),
|
||||
|
||||
@@ -82,9 +82,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
val iconUrl = BitmojiSelfie.getBitmojiSelfie(friendInfo?.bitmojiSelfieId, friendInfo?.bitmojiAvatarId, BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D)
|
||||
|
||||
val downloadLogging by context.config.downloader.logging
|
||||
if (downloadLogging.contains("started")) {
|
||||
context.shortToast(translations["download_started_toast"])
|
||||
}
|
||||
|
||||
val outputPath = createNewFilePath(
|
||||
context.config,
|
||||
|
||||
@@ -1,42 +1,13 @@
|
||||
package me.eternal.purrfectsnap.core.features.impl.downloader
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.Button
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AccountCircle
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Image
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color as ComposeColor
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader
|
||||
import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.AddViewEvent
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.NetworkApiRequestEvent
|
||||
import me.eternal.purrfectsnap.core.features.Feature
|
||||
import me.eternal.purrfectsnap.core.ui.PurrfectGlassCard
|
||||
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayPalette
|
||||
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayTheme
|
||||
import me.eternal.purrfectsnap.core.ui.ViewAppearanceHelper
|
||||
|
||||
class ProfilePictureDownloader : Feature("ProfilePictureDownloader") {
|
||||
@SuppressLint("SetTextI18n")
|
||||
@@ -51,153 +22,30 @@ class ProfilePictureDownloader : Feature("ProfilePictureDownloader") {
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.view::class.java.name != "com.snap.unifiedpublicprofile.UnifiedPublicProfileView") return@subscribe
|
||||
|
||||
event.parent.addView(ImageButton(event.parent.context).apply {
|
||||
val label = this@ProfilePictureDownloader.context.translation["profile_picture_downloader.button"]
|
||||
val density = resources.displayMetrics.density
|
||||
val sizePx = (44f * density).toInt()
|
||||
contentDescription = label
|
||||
setImageResource(android.R.drawable.stat_sys_download)
|
||||
setColorFilter(Color.WHITE)
|
||||
scaleType = ImageView.ScaleType.CENTER_INSIDE
|
||||
background = GradientDrawable().apply {
|
||||
shape = GradientDrawable.OVAL
|
||||
setColor(Color.parseColor("#332A2452"))
|
||||
setStroke(3, Color.parseColor("#66AFA3FF"))
|
||||
}
|
||||
setPadding(0, 0, 0, 0)
|
||||
layoutParams = RelativeLayout.LayoutParams(
|
||||
sizePx,
|
||||
sizePx
|
||||
).apply {
|
||||
event.parent.addView(Button(event.parent.context).apply {
|
||||
text = this@ProfilePictureDownloader.context.translation["profile_picture_downloader.button"]
|
||||
layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||
setMargins(0, 200, 0, 0)
|
||||
}
|
||||
setOnClickListener {
|
||||
val activity = this@ProfilePictureDownloader.context.mainActivity ?: return@setOnClickListener
|
||||
val translation = this@ProfilePictureDownloader.context.translation
|
||||
val options = buildList {
|
||||
backgroundUrl?.let { add("background_option" to it) }
|
||||
avatarUrl?.let { add("avatar_option" to it) }
|
||||
}
|
||||
createComposeAlertDialog(activity) { alertDialog ->
|
||||
PurrfectOverlayTheme {
|
||||
val dialogTitle = translation["profile_picture_downloader.title"]
|
||||
?: "Profile Picture Downloader"
|
||||
val subtitle = friendUsername ?: translation["profile_picture_downloader.subtitle"]
|
||||
?: "Choose which image to download"
|
||||
val border = remember {
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectOverlayPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
ViewAppearanceHelper.newAlertDialogBuilder(
|
||||
this@ProfilePictureDownloader.context.mainActivity!!
|
||||
).apply {
|
||||
setTitle(this@ProfilePictureDownloader.context.translation["profile_picture_downloader.title"])
|
||||
val choices = mutableMapOf<String, String>()
|
||||
backgroundUrl?.let { choices["background_option"] = it }
|
||||
avatarUrl?.let { choices["avatar_option"] = it }
|
||||
|
||||
setItems(choices.keys.map {
|
||||
this@ProfilePictureDownloader.context.translation["profile_picture_downloader.$it"]
|
||||
}.toTypedArray()) { _, which ->
|
||||
runCatching {
|
||||
this@ProfilePictureDownloader.context.feature(MediaDownloader::class).downloadProfilePicture(
|
||||
choices.values.elementAt(which),
|
||||
friendUsername!!
|
||||
)
|
||||
}
|
||||
val shape = RoundedCornerShape(26.dp)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 18.dp),
|
||||
shape = shape,
|
||||
color = ComposeColor.Transparent,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 18.dp,
|
||||
border = BorderStroke(1.dp, border)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(PurrfectOverlayPalette.cardOverlay, shape)
|
||||
.padding(horizontal = 18.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = ComposeColor.White.copy(alpha = 0.08f)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Download,
|
||||
contentDescription = null,
|
||||
tint = ComposeColor.White,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = dialogTitle,
|
||||
color = ComposeColor.White,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
color = PurrfectOverlayPalette.textSecondary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
if (options.isEmpty()) {
|
||||
Text(
|
||||
text = translation["profile_picture_downloader.no_images"]
|
||||
?: "No profile images found.",
|
||||
color = ComposeColor.White.copy(alpha = 0.8f),
|
||||
fontSize = 13.sp
|
||||
)
|
||||
} else {
|
||||
options.forEach { (key, url) ->
|
||||
val labelText = translation["profile_picture_downloader.$key"]
|
||||
val icon = if (key == "background_option") {
|
||||
Icons.Outlined.Image
|
||||
} else {
|
||||
Icons.Outlined.AccountCircle
|
||||
}
|
||||
Surface(
|
||||
onClick = {
|
||||
runCatching {
|
||||
this@ProfilePictureDownloader.context.feature(MediaDownloader::class).downloadProfilePicture(
|
||||
url,
|
||||
friendUsername!!
|
||||
)
|
||||
}.onFailure {
|
||||
this@ProfilePictureDownloader.context.log.error("Failed to download profile picture", it)
|
||||
}
|
||||
alertDialog.dismiss()
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = ComposeColor.White.copy(alpha = 0.05f),
|
||||
border = BorderStroke(1.dp, ComposeColor.White.copy(alpha = 0.12f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = ComposeColor.White.copy(alpha = 0.08f)
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
tint = ComposeColor.White,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = labelText ?: key,
|
||||
color = ComposeColor.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
this@ProfilePictureDownloader.context.log.error("Failed to download profile picture", it)
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
@@ -220,4 +68,4 @@ class ProfilePictureDownloader : Feature("ProfilePictureDownloader") {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class AppLock : Feature("AppLock") {
|
||||
private fun requestUnlock() {
|
||||
isUnlockRequested = true
|
||||
context.mainActivity!!.startActivityForResult(Intent().apply {
|
||||
component = ComponentName(Constants.SE_PACKAGE_NAME, "me.eternal.purrfectsnap.bridge.BiometricPromptActivity")
|
||||
component = ComponentName(Constants.MODULE_PACKAGE_NAME, "me.eternal.purrfectsnap.bridge.BiometricPromptActivity")
|
||||
}, requestCode)
|
||||
}
|
||||
|
||||
@@ -151,4 +151,4 @@ class AppLock : Feature("AppLock") {
|
||||
lock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class DeviceSpooferHook: Feature("Device Spoofer") {
|
||||
|
||||
private fun generateAndroidId(): String {
|
||||
if (spoofedAndroidId != null) return spoofedAndroidId!!
|
||||
val customId = context.config.experimental.spoof.customAndroidId.getNullable()
|
||||
val customId = context.config.experimental.spoof.spoofDeviceId.customAndroidId.getNullable()
|
||||
if (!customId.isNullOrEmpty()) {
|
||||
spoofedAndroidId = customId.lowercase()
|
||||
if (!hasLoggedId) {
|
||||
@@ -242,7 +242,7 @@ class DeviceSpooferHook: Feature("Device Spoofer") {
|
||||
val overridePlayStoreInstallerPackageName by context.config.experimental.spoof.overridePlayStoreInstallerPackageName
|
||||
val removeVpnTransportFlag by context.config.experimental.spoof.removeVpnTransportFlag
|
||||
val forceWifiTransportFlag by context.config.experimental.spoof.forceWifiTransportFlag
|
||||
val spoofAndroidId by context.config.experimental.spoof.spoofAndroidId
|
||||
val spoofAndroidId by context.config.experimental.spoof.spoofDeviceId.spoofAndroidId
|
||||
|
||||
if(overridePlayStoreInstallerPackageName) {
|
||||
hookInstallerPackageName()
|
||||
|
||||
@@ -188,7 +188,7 @@ class MediaFilePicker : Feature("Media File Picker") {
|
||||
|
||||
val isAudio = context.androidContext.contentResolver.getType(event.intent.data!!)!!.startsWith("audio/")
|
||||
|
||||
if (isAudio || context.config.messaging.galleryMediaSendOverride.getNullable() == null) {
|
||||
if (isAudio || context.config.messaging.galleryMediaSendOverride.mode.getNullable() == null) {
|
||||
startConversion(isAudio)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.AddViewEvent
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.UnaryCallEvent
|
||||
import me.eternal.purrfectsnap.core.features.Feature
|
||||
import me.eternal.purrfectsnap.core.ui.getComposerContext
|
||||
import me.eternal.purrfectsnap.core.ui.getComposerViewNode
|
||||
import me.eternal.purrfectsnap.core.ui.getValdiContext
|
||||
import me.eternal.purrfectsnap.core.ui.getValdiViewNode
|
||||
import me.eternal.purrfectsnap.core.util.ktx.getObjectField
|
||||
import me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID
|
||||
|
||||
@@ -35,23 +35,23 @@ class SnapScoreChanges: Feature("Snap Score Changes") {
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.viewClassName.endsWith("UnifiedProfileFlatlandProfileViewTopViewFrameLayout")) {
|
||||
val composerView = (event.view as ViewGroup).getChildAt(0) ?: return@subscribe
|
||||
val composerContext = composerView.getComposerContext() ?: return@subscribe
|
||||
val composerContext = composerView.getValdiContext() ?: return@subscribe
|
||||
|
||||
lastViewedUserId = composerContext.viewModel?.getObjectField("_userId")?.toString()
|
||||
}
|
||||
|
||||
if (event.viewClassName.endsWith("ProfileFlatlandFriendSnapScoreIdentityPillDialogView")) {
|
||||
event.view.post {
|
||||
event.view.getComposerContext()!!.enqueueNextRenderCallback {
|
||||
val composerViewNode = event.view.getComposerViewNode() ?: return@enqueueNextRenderCallback
|
||||
event.view.getValdiContext()!!.enqueueNextRenderCallback {
|
||||
val composerViewNode = event.view.getValdiViewNode() ?: return@enqueueNextRenderCallback
|
||||
val surface = composerViewNode.getChildren().getOrNull(1) ?: return@enqueueNextRenderCallback
|
||||
|
||||
val snapTextView = surface.getChildren().lastOrNull {
|
||||
it.getClassName() == "com.snap.composer.views.ComposerSnapTextView"
|
||||
it.getClassName().endsWith("SnapTextView")
|
||||
} ?: return@enqueueNextRenderCallback
|
||||
|
||||
|
||||
val currentFriendScore = scores[lastViewedUserId] ?: (event.view.getComposerContext()?.viewModel?.getObjectField("_friendSnapScore") as? Double)?.toLong() ?: return@enqueueNextRenderCallback
|
||||
val currentFriendScore = scores[lastViewedUserId] ?: (event.view.getValdiContext()?.viewModel?.getObjectField("_friendSnapScore") as? Double)?.toLong() ?: return@enqueueNextRenderCallback
|
||||
|
||||
val oldSnapScore = context.bridgeClient.getTracker().updateFriendScore(
|
||||
lastViewedUserId ?: return@enqueueNextRenderCallback,
|
||||
@@ -68,4 +68,4 @@ class SnapScoreChanges: Feature("Snap Score Changes") {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
package me.eternal.purrfectsnap.core.features.impl.experiments
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import me.eternal.purrfectsnap.common.bridge.FileHandleScope
|
||||
import me.eternal.purrfectsnap.common.bridge.toWrapper
|
||||
import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog
|
||||
import me.eternal.purrfectsnap.core.PurrfectSnap
|
||||
import me.eternal.purrfectsnap.core.features.Feature
|
||||
import me.eternal.purrfectsnap.core.features.impl.downloader.MediaDownloader
|
||||
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.wrapper.impl.valdi.ValdiFunction
|
||||
import me.eternal.purrfectsnap.core.wrapper.impl.valdi.ValdiMarshaller
|
||||
import me.eternal.purrfectsnap.nativelib.NativeLib
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.random.Random
|
||||
|
||||
class ValdiHooks: Feature("ValdiHooks") {
|
||||
private val config by lazy { context.config.experimental.nativeHooks.valdiHooks }
|
||||
private val getImportsFunctionName = Random.nextLong().absoluteValue.toString(16)
|
||||
|
||||
private var evalFunction: ValdiFunction? = null
|
||||
private val valdiConsole by lazy {
|
||||
createComposeAlertDialog(context.mainActivity!!) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
var result by remember { mutableStateOf("") }
|
||||
var codeContent by remember { mutableStateOf("1 + 2") }
|
||||
|
||||
Text("Valdi Console", fontSize = 18.sp, fontWeight = FontWeight.Bold)
|
||||
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textStyle = TextStyle.Default.copy(fontSize = 12.sp),
|
||||
value = codeContent,
|
||||
placeholder = { Text("Enter your JS code here:") },
|
||||
onValueChange = {
|
||||
codeContent = it
|
||||
}
|
||||
)
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
context.log.verbose("input: $codeContent", "ValdiConsole")
|
||||
result = "Running..."
|
||||
context.coroutineScope.launch {
|
||||
ValdiMarshaller.create()?.use { valdiMarshaller ->
|
||||
valdiMarshaller.pushUntyped(codeContent)
|
||||
valdiMarshaller.pushUntyped(newValdiFunction {
|
||||
if (getSize() < 1) return@newValdiFunction false
|
||||
val output = getUntyped(0)
|
||||
context.log.verbose("eval: $output", "ValdiConsole")
|
||||
|
||||
result = if (output is Exception) {
|
||||
"${output.javaClass.simpleName}: ${output.message}"
|
||||
} else {
|
||||
output?.toString() ?: "undefined"
|
||||
}
|
||||
true
|
||||
})
|
||||
|
||||
evalFunction?.perform(valdiMarshaller)
|
||||
} ?: run {
|
||||
result = "Failed to create ValdiMarshaller"
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("Run")
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun newValdiFunction(block: ValdiMarshaller.() -> Boolean): Any? {
|
||||
val functionClass = PurrfectSnap.classCache.valdiFunction ?: return null
|
||||
return java.lang.reflect.Proxy.newProxyInstance(
|
||||
functionClass.classLoader,
|
||||
arrayOf(functionClass)
|
||||
) { _, method, args ->
|
||||
if (method.name != "perform") return@newProxyInstance null
|
||||
block(ValdiMarshaller(args?.get(0) ?: return@newProxyInstance false))
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun init() {
|
||||
if (config.globalState != true) return
|
||||
|
||||
if (PurrfectSnap.classCache.valdiFunction == null) {
|
||||
context.log.warn("ComposerFunction/ValdiFunction class not found, ValdiHooks feature disabled")
|
||||
return
|
||||
}
|
||||
|
||||
val importedFunctions = mutableMapOf<String, Any?>()
|
||||
|
||||
fun valdiFunction(name: String, block: ValdiMarshaller.() -> Unit) {
|
||||
val function = newValdiFunction {
|
||||
block(this)
|
||||
true
|
||||
}
|
||||
if (function != null) {
|
||||
importedFunctions[name] = function
|
||||
}
|
||||
}
|
||||
|
||||
valdiFunction("getConfig") {
|
||||
pushUntyped(mapOf<String, Any>(
|
||||
"operaDownloadButton" to context.config.downloader.operaDownloadButton.get(),
|
||||
"bypassCameraRollLimit" to config.bypassCameraRollLimit.get(),
|
||||
"showFirstCreatedUsername" to config.showFirstCreatedUsername.get(),
|
||||
"valdiLogs" to config.valdiLogs.get(),
|
||||
"customSelfDestructSnapDelay" to config.customSelfDestructSnapDelay.get(),
|
||||
))
|
||||
}
|
||||
|
||||
valdiFunction("showToast") {
|
||||
if (getSize() < 1) return@valdiFunction
|
||||
context.shortToast(getUntyped(0) as? String ?: return@valdiFunction)
|
||||
}
|
||||
|
||||
valdiFunction("downloadLastOperaMedia") {
|
||||
context.feature(MediaDownloader::class).downloadLastOperaMediaAsync(getUntyped(0) == true)
|
||||
}
|
||||
|
||||
valdiFunction("getFriendOriginalUsername") {
|
||||
if (getSize() < 1) return@valdiFunction
|
||||
val username = getUntyped(0) as? String ?: return@valdiFunction
|
||||
|
||||
runCatching {
|
||||
pushUntyped(context.database.getFriendOriginalUsername(username))
|
||||
}.onFailure {
|
||||
pushUntyped(null)
|
||||
}
|
||||
}
|
||||
|
||||
valdiFunction("log") {
|
||||
if (getSize() < 2) return@valdiFunction
|
||||
val logLevel = getUntyped(0) as? String ?: return@valdiFunction
|
||||
val message = getUntyped(1) as? String ?: return@valdiFunction
|
||||
|
||||
val tag = "ValdiLogs"
|
||||
|
||||
when (logLevel) {
|
||||
"log" -> context.log.verbose(message, tag)
|
||||
"debug" -> context.log.debug(message, tag)
|
||||
"info" -> context.log.info(message, tag)
|
||||
"warn" -> context.log.warn(message, tag)
|
||||
"error" -> context.log.error(message, tag)
|
||||
}
|
||||
}
|
||||
|
||||
valdiFunction("setEvalFunction") {
|
||||
if (getSize() < 1) return@valdiFunction
|
||||
evalFunction = ValdiFunction(getUntyped(0) ?: return@valdiFunction)
|
||||
context.log.verbose("Set eval function: $evalFunction", "ValdiHooks")
|
||||
}
|
||||
|
||||
fun loadHooks() {
|
||||
if (!NativeLib.initialized) {
|
||||
context.log.error("ValdiHooks cannot be loaded without NativeLib")
|
||||
return
|
||||
}
|
||||
val loaderScript = runCatching {
|
||||
context.fileHandlerManager.getFileHandle(FileHandleScope.VALDI.key, "loader.js").toWrapper().readBytes().toString(Charsets.UTF_8)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to load valdi loader script", it)
|
||||
}.getOrNull() ?: return
|
||||
context.native.setValdiLoader("""
|
||||
const i = setInterval(() => {
|
||||
try {
|
||||
const _runtimeName = "${if (PurrfectSnap.classCache.nativeBridge.name == "com.snapchat.client.valdi.NativeBridge") "valdi" else "composer"}";
|
||||
require(_runtimeName + '_core/src/DeviceBridge').getDisplayWidth();
|
||||
clearInterval(i);
|
||||
(() => { const _getImportsFunctionName = "$getImportsFunctionName"; $loaderScript })();
|
||||
} catch (e) {}
|
||||
}, 200)
|
||||
""".trimIndent().trim())
|
||||
}
|
||||
|
||||
loadHooks()
|
||||
|
||||
if (config.valdiConsole.get()) {
|
||||
context.inAppOverlay.addCustomComposable {
|
||||
FilledIconButton(
|
||||
onClick = {
|
||||
valdiConsole.show()
|
||||
},
|
||||
modifier = Modifier.align(Alignment.TopEnd).padding(top = 100.dp, end = 16.dp)
|
||||
) {
|
||||
Icon(Icons.Default.BugReport, contentDescription = "Debug Console")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PurrfectSnap.classCache.nativeBridge.hook("registerNativeModuleFactory", HookStage.BEFORE) { param ->
|
||||
val moduleFactory = param.argNullable<Any>(1) ?: return@hook
|
||||
if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString()?.contains("DeviceBridge") != true) return@hook
|
||||
Hooker.ephemeralHookObjectMethod(moduleFactory.javaClass, moduleFactory, "loadModule", HookStage.AFTER) { methodParam ->
|
||||
val result = methodParam.getResult() as? MutableMap<String, Any?> ?: return@ephemeralHookObjectMethod
|
||||
val importsFunction = newValdiFunction {
|
||||
pushUntyped(importedFunctions)
|
||||
true
|
||||
}
|
||||
if (importsFunction != null) {
|
||||
result[getImportsFunctionName] = importsFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user