diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b86273d
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..ca16a99
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/markdown.xml b/.idea/markdown.xml
new file mode 100644
index 0000000..c61ea33
--- /dev/null
+++ b/.idea/markdown.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7374e5b..0d48aad 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
{
+ return prefs.getStringSet(KEY, emptySet()) ?: emptySet()
+ }
+
+ fun addApp(prefs: SharedPreferences, packageName: String) {
+ val current = getBlocklist(prefs).toMutableSet()
+ current.add(packageName)
+ prefs.edit().putStringSet(KEY, current).apply()
+ }
+
+ fun removeApp(prefs: SharedPreferences, packageName: String) {
+ val current = getBlocklist(prefs).toMutableSet()
+ current.remove(packageName)
+ prefs.edit().putStringSet(KEY, current).apply()
+ }
+
+ fun isBlocked(prefs: SharedPreferences, packageName: String): Boolean {
+ return packageName in getBlocklist(prefs)
+ }
+}
diff --git a/app/src/main/java/com/musheer360/swiftslate/manager/CommandManager.kt b/app/src/main/java/com/musheer360/swiftslate/manager/CommandManager.kt
index 5066a7d..c79d9eb 100644
--- a/app/src/main/java/com/musheer360/swiftslate/manager/CommandManager.kt
+++ b/app/src/main/java/com/musheer360/swiftslate/manager/CommandManager.kt
@@ -2,11 +2,13 @@ package com.musheer360.swiftslate.manager
import android.content.Context
import android.content.SharedPreferences
+import androidx.compose.runtime.Stable
import com.musheer360.swiftslate.model.Command
import com.musheer360.swiftslate.model.CommandType
import org.json.JSONArray
import org.json.JSONObject
+@Stable
class CommandManager(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences("commands", Context.MODE_PRIVATE)
private val settingsPrefs: SharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
diff --git a/app/src/main/java/com/musheer360/swiftslate/manager/KeyManager.kt b/app/src/main/java/com/musheer360/swiftslate/manager/KeyManager.kt
index f98af36..950b2cb 100644
--- a/app/src/main/java/com/musheer360/swiftslate/manager/KeyManager.kt
+++ b/app/src/main/java/com/musheer360/swiftslate/manager/KeyManager.kt
@@ -5,6 +5,7 @@ import android.content.SharedPreferences
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
+import androidx.compose.runtime.Stable
import org.json.JSONArray
import java.nio.charset.StandardCharsets
import java.security.KeyStore
@@ -14,6 +15,7 @@ import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
+@Stable
class KeyManager(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences("secure_keys_prefs", Context.MODE_PRIVATE)
diff --git a/app/src/main/java/com/musheer360/swiftslate/manager/StatsManager.kt b/app/src/main/java/com/musheer360/swiftslate/manager/StatsManager.kt
index c241020..796f0d7 100644
--- a/app/src/main/java/com/musheer360/swiftslate/manager/StatsManager.kt
+++ b/app/src/main/java/com/musheer360/swiftslate/manager/StatsManager.kt
@@ -2,10 +2,12 @@ package com.musheer360.swiftslate.manager
import android.content.Context
import android.content.SharedPreferences
+import androidx.compose.runtime.Stable
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.Locale
+@Stable
class StatsManager(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences("stats", Context.MODE_PRIVATE)
diff --git a/app/src/main/java/com/musheer360/swiftslate/model/AppInfo.kt b/app/src/main/java/com/musheer360/swiftslate/model/AppInfo.kt
new file mode 100644
index 0000000..d38d366
--- /dev/null
+++ b/app/src/main/java/com/musheer360/swiftslate/model/AppInfo.kt
@@ -0,0 +1,17 @@
+package com.musheer360.swiftslate.model
+
+import android.content.SharedPreferences
+import android.graphics.drawable.Drawable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+
+@Immutable
+data class AppInfo(
+ val name: String,
+ val packageName: String,
+ val icon: Drawable
+)
+
+@Stable
+class StablePrefs(val prefs: SharedPreferences)
+
diff --git a/app/src/main/java/com/musheer360/swiftslate/service/AssistantService.kt b/app/src/main/java/com/musheer360/swiftslate/service/AssistantService.kt
index c10ef69..0ba2835 100644
--- a/app/src/main/java/com/musheer360/swiftslate/service/AssistantService.kt
+++ b/app/src/main/java/com/musheer360/swiftslate/service/AssistantService.kt
@@ -30,6 +30,7 @@ import com.musheer360.swiftslate.api.ApiException
import com.musheer360.swiftslate.api.GeminiClient
import com.musheer360.swiftslate.api.GenerateResult
import com.musheer360.swiftslate.api.OpenAICompatibleClient
+import com.musheer360.swiftslate.manager.BlocklistManager
import com.musheer360.swiftslate.manager.CommandManager
import com.musheer360.swiftslate.manager.KeyManager
import com.musheer360.swiftslate.manager.StatsManager
@@ -163,9 +164,13 @@ class AssistantService : AccessibilityService() {
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
if (event?.eventType != AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED) return
- if (event.packageName?.toString() == packageName) return
+ val eventPackage = event.packageName?.toString() ?: return
+ if (eventPackage == packageName) return
if (!::keyManager.isInitialized) return
+ val prefs = applicationContext.getSharedPreferences("settings", Context.MODE_PRIVATE)
+ if (BlocklistManager.isBlocked(prefs, eventPackage)) return
+
if (isProcessing.get()) return
val source = event.source ?: return
if (source.isPassword) {
diff --git a/app/src/main/java/com/musheer360/swiftslate/ui/BlocklistScreen.kt b/app/src/main/java/com/musheer360/swiftslate/ui/BlocklistScreen.kt
new file mode 100644
index 0000000..aecfccf
--- /dev/null
+++ b/app/src/main/java/com/musheer360/swiftslate/ui/BlocklistScreen.kt
@@ -0,0 +1,296 @@
+package com.musheer360.swiftslate.ui
+
+import android.content.pm.PackageManager
+import android.widget.ImageView
+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.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+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.Close
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.SwitchDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+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.graphics.SolidColor
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.res.stringResource
+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.viewinterop.AndroidView
+import com.musheer360.swiftslate.R
+import com.musheer360.swiftslate.manager.BlocklistManager
+import com.musheer360.swiftslate.model.AppInfo
+import com.musheer360.swiftslate.model.StablePrefs
+import com.musheer360.swiftslate.ui.components.SlateCard
+import com.musheer360.swiftslate.ui.components.SlateItemCard
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+@Composable
+fun BlocklistScreen(prefs: StablePrefs, onBack: () -> Unit) {
+ val context = LocalContext.current
+ val haptic = LocalHapticFeedback.current
+
+ var installedApps by remember { mutableStateOf>(emptyList()) }
+ var isLoading by remember { mutableStateOf(true) }
+ var searchQuery by rememberSaveable { mutableStateOf("") }
+ var blocklist by remember { mutableStateOf(BlocklistManager.getBlocklist(prefs.prefs)) }
+
+ // Load apps asynchronously on launch
+ LaunchedEffect(Unit) {
+ withContext(Dispatchers.IO) {
+ val pm = context.packageManager
+ val apps = pm.getInstalledApplications(PackageManager.GET_META_DATA)
+ .map { app ->
+ AppInfo(
+ name = app.loadLabel(pm).toString(),
+ packageName = app.packageName,
+ icon = app.loadIcon(pm)
+ )
+ }
+ .sortedBy { it.name.lowercase() }
+
+ withContext(Dispatchers.Main) {
+ installedApps = apps
+ isLoading = false
+ }
+ }
+ }
+
+ val filteredApps = remember(installedApps, searchQuery) {
+ if (searchQuery.isBlank()) installedApps
+ else installedApps.filter {
+ it.name.contains(searchQuery, ignoreCase = true) ||
+ it.packageName.contains(searchQuery, ignoreCase = true)
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .graphicsLayer { }
+ .padding(horizontal = 20.dp, vertical = 16.dp)
+ ) {
+ // Top row with ArrowBack and Title
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 20.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ IconButton(
+ onClick = {
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ onBack()
+ },
+ modifier = Modifier.padding(end = 8.dp)
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(R.string.commands_cancel),
+ tint = MaterialTheme.colorScheme.onBackground
+ )
+ }
+ Text(
+ text = stringResource(R.string.settings_blocklist_title),
+ fontSize = 28.sp,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onBackground
+ )
+ }
+
+ // Subtitle Description
+ Text(
+ text = stringResource(R.string.settings_blocklist_desc),
+ fontSize = 14.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+
+ // Search Pill
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 8.dp),
+ shape = RoundedCornerShape(10.dp),
+ color = MaterialTheme.colorScheme.surface
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 44.dp)
+ .padding(horizontal = 12.dp, vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ BasicTextField(
+ value = searchQuery,
+ onValueChange = { searchQuery = it },
+ singleLine = true,
+ textStyle = LocalTextStyle.current.copy(
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onSurface
+ ),
+ cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
+ modifier = Modifier.weight(1f),
+ decorationBox = { innerTextField ->
+ Box {
+ if (searchQuery.isEmpty()) {
+ Text(
+ text = stringResource(R.string.blocklist_search_placeholder),
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ innerTextField()
+ }
+ }
+ )
+ if (searchQuery.isNotEmpty()) {
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = stringResource(R.string.commands_search_close),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier
+ .size(18.dp)
+ .clickable(interactionSource = null, indication = null) {
+ searchQuery = ""
+ }
+ )
+ }
+ }
+ }
+
+ // App List
+ SlateCard(
+ modifier = Modifier.weight(1f),
+ fillHeight = true
+ ) {
+ if (isLoading) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ } else if (filteredApps.isEmpty()) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = stringResource(R.string.blocklist_empty),
+ fontSize = 14.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center
+ )
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(8.dp)),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ contentPadding = PaddingValues(vertical = 4.dp)
+ ) {
+ items(filteredApps, key = { it.packageName }) { app ->
+ val isBlocked = blocklist.contains(app.packageName)
+ SlateItemCard {
+ // Native image icon for flawless rendering
+ AndroidView(
+ factory = { ctx ->
+ ImageView(ctx).apply {
+ scaleType = ImageView.ScaleType.FIT_CENTER
+ }
+ },
+ modifier = Modifier.size(40.dp),
+ update = { imageView ->
+ imageView.setImageDrawable(app.icon)
+ }
+ )
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = app.name,
+ fontWeight = FontWeight.Bold,
+ fontSize = 15.sp,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = app.packageName,
+ fontSize = 12.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ Switch(
+ checked = isBlocked,
+ onCheckedChange = { checkState ->
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ if (checkState) {
+ BlocklistManager.addApp(prefs.prefs, app.packageName)
+ } else {
+ BlocklistManager.removeApp(prefs.prefs, app.packageName)
+ }
+ blocklist = BlocklistManager.getBlocklist(prefs.prefs)
+ },
+ colors = SwitchDefaults.colors(
+ checkedThumbColor = MaterialTheme.colorScheme.primary,
+ checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
+ uncheckedThumbColor = MaterialTheme.colorScheme.outline,
+ uncheckedTrackColor = MaterialTheme.colorScheme.surface
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/musheer360/swiftslate/ui/KeysScreen.kt b/app/src/main/java/com/musheer360/swiftslate/ui/KeysScreen.kt
index 037e14c..f5cfde4 100644
--- a/app/src/main/java/com/musheer360/swiftslate/ui/KeysScreen.kt
+++ b/app/src/main/java/com/musheer360/swiftslate/ui/KeysScreen.kt
@@ -3,21 +3,38 @@ package com.musheer360.swiftslate.ui
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
-import android.content.SharedPreferences
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.heightIn
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+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.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
@@ -30,6 +47,7 @@ import com.musheer360.swiftslate.api.GeminiClient
import com.musheer360.swiftslate.api.OpenAICompatibleClient
import com.musheer360.swiftslate.manager.KeyManager
import com.musheer360.swiftslate.model.ProviderType
+import com.musheer360.swiftslate.model.StablePrefs
import com.musheer360.swiftslate.ui.components.ScreenTitle
import com.musheer360.swiftslate.ui.components.SlateCard
import com.musheer360.swiftslate.ui.components.SlateItemCard
@@ -37,7 +55,7 @@ import com.musheer360.swiftslate.ui.components.SlateTextField
import kotlinx.coroutines.launch
@Composable
-fun KeysScreen(keyManager: KeyManager, prefs: SharedPreferences) {
+fun KeysScreen(keyManager: KeyManager, prefs: StablePrefs) {
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val uriHandler = LocalUriHandler.current
@@ -99,13 +117,21 @@ fun KeysScreen(keyManager: KeyManager, prefs: SharedPreferences) {
return@launch
}
val result = run {
- val providerType = prefs.getString("provider_type", ProviderType.GEMINI) ?: ProviderType.GEMINI
- val customEndpoint = prefs.getString("custom_endpoint", "") ?: ""
+ val providerType =
+ prefs.prefs.getString("provider_type", ProviderType.GEMINI)
+ ?: ProviderType.GEMINI
+ val customEndpoint =
+ prefs.prefs.getString("custom_endpoint", "") ?: ""
when {
providerType == ProviderType.GROQ ->
- openAIClient.validateKey(trimmedKey, "https://api.groq.com/openai/v1")
+ openAIClient.validateKey(
+ trimmedKey,
+ "https://api.groq.com/openai/v1"
+ )
+
providerType == ProviderType.CUSTOM && customEndpoint.isNotBlank() ->
openAIClient.validateKey(trimmedKey, customEndpoint)
+
else ->
geminiClient.validateKey(trimmedKey)
}
@@ -122,10 +148,12 @@ fun KeysScreen(keyManager: KeyManager, prefs: SharedPreferences) {
testResult = validAddedMsg
testSuccess = true
// Clear clipboard to prevent API key leaking via paste history
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clipboard =
+ context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("", ""))
} else {
- testResult = result.exceptionOrNull()?.message ?: validationFailedMsg
+ testResult =
+ result.exceptionOrNull()?.message ?: validationFailedMsg
testSuccess = false
}
}
@@ -133,7 +161,9 @@ fun KeysScreen(keyManager: KeyManager, prefs: SharedPreferences) {
},
enabled = newKey.isNotBlank() && !isTesting && keyManager.keystoreAvailable,
shape = RoundedCornerShape(10.dp),
- modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 48.dp)
) {
Text(if (isTesting) stringResource(R.string.keys_testing) else stringResource(R.string.keys_add_key))
}
@@ -145,7 +175,10 @@ fun KeysScreen(keyManager: KeyManager, prefs: SharedPreferences) {
modifier = Modifier.padding(top = 8.dp)
)
}
- val (apiKeyUrl, providerName) = when (prefs.getString("provider_type", ProviderType.GEMINI) ?: ProviderType.GEMINI) {
+ val (apiKeyUrl, providerName) = when (prefs.prefs.getString(
+ "provider_type",
+ ProviderType.GEMINI
+ ) ?: ProviderType.GEMINI) {
ProviderType.GROQ -> "https://console.groq.com/keys" to "Groq"
ProviderType.CUSTOM -> null to null
else -> "https://aistudio.google.com/api-keys" to "Gemini"
@@ -156,7 +189,10 @@ fun KeysScreen(keyManager: KeyManager, prefs: SharedPreferences) {
color = MaterialTheme.colorScheme.primary,
fontSize = 13.sp,
modifier = Modifier
- .clickable(interactionSource = null, indication = null) { uriHandler.openUri(apiKeyUrl) }
+ .clickable(
+ interactionSource = null,
+ indication = null
+ ) { uriHandler.openUri(apiKeyUrl) }
.padding(top = 8.dp)
)
}
@@ -167,18 +203,24 @@ fun KeysScreen(keyManager: KeyManager, prefs: SharedPreferences) {
if (keys.isNotEmpty()) {
SlateCard(modifier = Modifier.weight(1f)) {
LazyColumn(
- modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(8.dp)),
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(RoundedCornerShape(8.dp)),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(bottom = 4.dp)
) {
- itemsIndexed(keys, key = { index, k -> "$index-${k.hashCode()}" }) { index, key ->
+ itemsIndexed(
+ keys,
+ key = { index, k -> "$index-${k.hashCode()}" }) { index, key ->
SlateItemCard {
Text(
text = "••••••••" + key.takeLast(4),
fontWeight = FontWeight.Medium,
fontSize = 15.sp,
color = MaterialTheme.colorScheme.onSurface,
- modifier = Modifier.weight(1f).semantics(mergeDescendants = true) {}
+ modifier = Modifier
+ .weight(1f)
+ .semantics(mergeDescendants = true) {}
)
Text(
text = stringResource(R.string.delete_confirm_button),
@@ -229,7 +271,10 @@ fun KeysScreen(keyManager: KeyManager, prefs: SharedPreferences) {
}
keyToDelete = null
}) {
- Text(stringResource(R.string.delete_confirm_button), color = MaterialTheme.colorScheme.error)
+ Text(
+ stringResource(R.string.delete_confirm_button),
+ color = MaterialTheme.colorScheme.error
+ )
}
},
dismissButton = {
diff --git a/app/src/main/java/com/musheer360/swiftslate/ui/SettingsScreen.kt b/app/src/main/java/com/musheer360/swiftslate/ui/SettingsScreen.kt
index 96768ff..ec6b31f 100644
--- a/app/src/main/java/com/musheer360/swiftslate/ui/SettingsScreen.kt
+++ b/app/src/main/java/com/musheer360/swiftslate/ui/SettingsScreen.kt
@@ -1,11 +1,13 @@
package com.musheer360.swiftslate.ui
-import android.content.SharedPreferences
+import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.rememberCoroutineScope
@@ -28,8 +30,10 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import java.util.Locale
import com.musheer360.swiftslate.manager.CommandManager
import com.musheer360.swiftslate.model.ProviderType
+import com.musheer360.swiftslate.model.StablePrefs
import com.musheer360.swiftslate.ui.components.ScreenTitle
import com.musheer360.swiftslate.ui.components.SlateCard
import com.musheer360.swiftslate.ui.components.SlateDivider
@@ -37,33 +41,39 @@ import com.musheer360.swiftslate.ui.components.SlateTextField
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
+fun SettingsScreen(commandManager: CommandManager, prefs: StablePrefs) {
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val uriHandler = LocalUriHandler.current
+ var showBlocklist by remember { mutableStateOf(false) }
+
+ BackHandler(enabled = showBlocklist) {
+ showBlocklist = false
+ }
+
val scope = rememberCoroutineScope()
var saveEndpointJob by remember { mutableStateOf(null) }
var saveModelJob by remember { mutableStateOf(null) }
- var providerType by remember { mutableStateOf(prefs.getString("provider_type", ProviderType.GEMINI) ?: ProviderType.GEMINI) }
+ var providerType by remember { mutableStateOf(prefs.prefs.getString("provider_type", ProviderType.GEMINI) ?: ProviderType.GEMINI) }
var providerExpanded by remember { mutableStateOf(false) }
- var selectedModel by remember { mutableStateOf(prefs.getString("model", "gemini-2.5-flash-lite") ?: "gemini-2.5-flash-lite") }
+ var selectedModel by remember { mutableStateOf(prefs.prefs.getString("model", "gemini-2.5-flash-lite") ?: "gemini-2.5-flash-lite") }
var modelExpanded by remember { mutableStateOf(false) }
val geminiModels = listOf("gemini-2.5-flash-lite", "gemini-3-flash-preview", "gemini-3.1-flash-lite-preview")
- var groqModel by remember { mutableStateOf(prefs.getString("groq_model", "llama-3.3-70b-versatile") ?: "llama-3.3-70b-versatile") }
+ var groqModel by remember { mutableStateOf(prefs.prefs.getString("groq_model", "llama-3.3-70b-versatile") ?: "llama-3.3-70b-versatile") }
var groqModelExpanded by remember { mutableStateOf(false) }
val groqModels = listOf("llama-3.3-70b-versatile", "llama-3.1-8b-instant", "openai/gpt-oss-120b", "openai/gpt-oss-20b", "meta-llama/llama-4-scout-17b-16e-instruct")
- var customEndpoint by rememberSaveable { mutableStateOf(prefs.getString("custom_endpoint", "") ?: "") }
- var customModel by rememberSaveable { mutableStateOf(prefs.getString("custom_model", "") ?: "") }
+ var customEndpoint by rememberSaveable { mutableStateOf(prefs.prefs.getString("custom_endpoint", "") ?: "") }
+ var customModel by rememberSaveable { mutableStateOf(prefs.prefs.getString("custom_model", "") ?: "") }
var endpointError by remember { mutableStateOf(null) }
var triggerPrefix by remember { mutableStateOf(commandManager.getTriggerPrefix()) }
var prefixError by remember { mutableStateOf(null) }
- var temperature by remember { mutableStateOf(prefs.getFloat("temperature", 0.5f)) }
+ var temperature by remember { mutableStateOf(prefs.prefs.getFloat("temperature", 0.5f)) }
val prefixErrorLength = stringResource(R.string.settings_prefix_error_length)
val prefixErrorWhitespace = stringResource(R.string.settings_prefix_error_whitespace)
@@ -79,9 +89,9 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
onDispose {
saveEndpointJob?.cancel()
saveModelJob?.cancel()
- val editor = prefs.edit()
+ val editor = prefs.prefs.edit()
var needsWrite = false
- if (customEndpoint != (prefs.getString("custom_endpoint", "") ?: "")) {
+ if (customEndpoint != (prefs.prefs.getString("custom_endpoint", "") ?: "")) {
val isValid = customEndpoint.isBlank() || customEndpoint.startsWith("https://") ||
(customEndpoint.startsWith("http://") && try {
val host = java.net.URL(customEndpoint).host
@@ -92,7 +102,7 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
needsWrite = true
}
}
- if (customModel != (prefs.getString("custom_model", "") ?: "")) {
+ if (customModel != (prefs.prefs.getString("custom_model", "") ?: "")) {
editor.putString("custom_model", customModel)
needsWrite = true
}
@@ -148,12 +158,15 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
}
}
- Column(
- modifier = Modifier
- .fillMaxSize()
- .graphicsLayer { }
- .padding(horizontal = 20.dp, vertical = 16.dp)
- ) {
+ if (showBlocklist) {
+ BlocklistScreen(prefs = prefs, onBack = { showBlocklist = false })
+ } else {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .graphicsLayer { }
+ .padding(horizontal = 20.dp, vertical = 16.dp)
+ ) {
ScreenTitle(stringResource(R.string.settings_title))
// Card 1: Provider + Model
@@ -190,7 +203,7 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
providerType = ProviderType.GEMINI
- prefs.edit().putString("provider_type", ProviderType.GEMINI).remove("structured_output_disabled_at").apply()
+ prefs.prefs.edit().putString("provider_type", ProviderType.GEMINI).remove("structured_output_disabled_at").apply()
providerExpanded = false
}
)
@@ -199,7 +212,7 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
providerType = ProviderType.GROQ
- prefs.edit().putString("provider_type", ProviderType.GROQ).remove("structured_output_disabled_at").apply()
+ prefs.prefs.edit().putString("provider_type", ProviderType.GROQ).remove("structured_output_disabled_at").apply()
providerExpanded = false
}
)
@@ -208,7 +221,7 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
providerType = ProviderType.CUSTOM
- prefs.edit().putString("provider_type", ProviderType.CUSTOM).remove("structured_output_disabled_at").apply()
+ prefs.prefs.edit().putString("provider_type", ProviderType.CUSTOM).remove("structured_output_disabled_at").apply()
providerExpanded = false
}
)
@@ -245,7 +258,7 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
selectedModel = model
- prefs.edit().putString("model", model).remove("structured_output_disabled_at").apply()
+ prefs.prefs.edit().putString("model", model).remove("structured_output_disabled_at").apply()
modelExpanded = false
}
)
@@ -282,7 +295,7 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
groqModel = model
- prefs.edit().putString("groq_model", model).remove("structured_output_disabled_at").apply()
+ prefs.prefs.edit().putString("groq_model", model).remove("structured_output_disabled_at").apply()
groqModelExpanded = false
}
)
@@ -315,7 +328,7 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
saveEndpointJob?.cancel()
saveEndpointJob = scope.launch {
delay(500)
- prefs.edit().putString("custom_endpoint", it).remove("structured_output_disabled_at").apply()
+ prefs.prefs.edit().putString("custom_endpoint", it).remove("structured_output_disabled_at").apply()
}
}
},
@@ -345,7 +358,7 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
saveModelJob?.cancel()
saveModelJob = scope.launch {
delay(500)
- prefs.edit().putString("custom_model", it).remove("structured_output_disabled_at").apply()
+ prefs.prefs.edit().putString("custom_model", it).remove("structured_output_disabled_at").apply()
}
},
placeholder = { Text(stringResource(R.string.settings_model_placeholder)) },
@@ -364,7 +377,7 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
)
Spacer(modifier = Modifier.weight(1f))
Text(
- text = String.format("%.1f", temperature),
+ text = String.format(Locale.US, "%.1f", temperature),
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
@@ -381,7 +394,7 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
}
},
onValueChangeFinished = {
- prefs.edit().putFloat("temperature", temperature).apply()
+ prefs.prefs.edit().putFloat("temperature", temperature).apply()
},
valueRange = 0f..2f,
steps = 19,
@@ -442,6 +455,43 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
Spacer(modifier = Modifier.height(8.dp))
+ // Card 2.5: App Blocklist
+ SlateCard(
+ modifier = Modifier.clickable {
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ showBlocklist = true
+ }
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = stringResource(R.string.settings_blocklist_title),
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = stringResource(R.string.settings_blocklist_desc),
+ fontSize = 13.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowForward,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
// Card 3: Backup
SlateCard {
Text(
@@ -545,4 +595,5 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) {
}
)
}
+ }
}
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 8f8a978..08980f4 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -89,4 +89,10 @@
Temperatur
Mit Liebe gemacht von Musheer Alam
Auf GitHub sponsern
+
+
+ App-Blockliste
+ Wählen Sie Apps, in denen SwiftSlate deaktiviert sein soll.
+ Apps suchen…
+ Keine Apps gefunden
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index b3276bf..44194e8 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -89,4 +89,10 @@
Temperatura
Hecho con amor por Musheer Alam
Patrocinar en GitHub
+
+
+ Lista negra de aplicaciones
+ Selecciona las aplicaciones donde se debe desactivar SwiftSlate.
+ Buscar aplicaciones…
+ No se encontraron aplicaciones
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index b18197a..9933500 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -89,4 +89,10 @@
Température
Fait avec amour par Musheer Alam
Sponsoriser sur GitHub
+
+
+ Liste noire d\'applications
+ Sélectionnez les applications à désactiver.
+ Rechercher des applications…
+ Aucune application trouvée
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index d41ab1c..e4ea1ff 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -89,4 +89,10 @@
तापमान
Musheer Alam द्वारा प्यार से बनाया गया
GitHub पर प्रायोजित करें
+
+
+ ऐप ब्लॉकलिस्ट
+ उन ऐप्स को चुनें जहां SwiftSlate निष्क्रिय होना चाहिए।
+ ऐप्स खोजें…
+ कोई ऐप नहीं मिला
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index cc86f6f..f9b8a05 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -89,4 +89,10 @@
Temperatura
Feito com amor por Musheer Alam
Patrocinar no GitHub
+
+
+ Lista de bloqueio de apps
+ Selecione os aplicativos onde o SwiftSlate deve ser desativado.
+ Buscar aplicativos…
+ Nenhum aplicativo encontrado
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 613dd8b..6aaa446 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -89,4 +89,10 @@
温度
由 Musheer Alam 用心制作
在 GitHub 上赞助
+
+
+ 应用黑名单
+ 选择要停用 SwiftSlate 的应用。
+ 搜索应用…
+ 未找到应用
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7fa2167..9dc8cd5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -89,4 +89,10 @@
Delete this API key?
This action cannot be undone.
Delete
+
+
+ App Blocklist
+ Select apps where SwiftSlate should be disabled.
+ Search apps…
+ No apps found
diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties
new file mode 100644
index 0000000..6c1139e
--- /dev/null
+++ b/gradle/gradle-daemon-jvm.properties
@@ -0,0 +1,12 @@
+#This file is generated by updateDaemonJvm
+toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
+toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
+toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
+toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
+toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
+toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
+toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
+toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
+toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
+toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
+toolchainVersion=21