From 74a26051cc6e406729abb20de7c7a34320438a1e Mon Sep 17 00:00:00 2001 From: Firad Date: Sun, 24 May 2026 17:40:36 +0600 Subject: [PATCH 1/2] feature add --- .idea/.gitignore | 3 + .idea/AndroidProjectSystem.xml | 6 + .idea/compiler.xml | 6 + .idea/deploymentTargetSelector.xml | 11 + .idea/markdown.xml | 8 + .idea/runConfigurations.xml | 17 ++ .idea/vcs.xml | 6 + .../swiftslate/manager/BlocklistManager.kt | 27 ++ .../musheer360/swiftslate/model/AppInfo.kt | 11 + .../swiftslate/service/AssistantService.kt | 7 +- .../swiftslate/ui/BlocklistScreen.kt | 277 ++++++++++++++++++ .../swiftslate/ui/SettingsScreen.kt | 62 +++- app/src/main/res/values-de/strings.xml | 6 + app/src/main/res/values-es/strings.xml | 6 + app/src/main/res/values-fr/strings.xml | 6 + app/src/main/res/values-hi/strings.xml | 6 + app/src/main/res/values-pt-rBR/strings.xml | 6 + app/src/main/res/values-zh-rCN/strings.xml | 6 + app/src/main/res/values/strings.xml | 6 + gradle/gradle-daemon-jvm.properties | 12 + 20 files changed, 488 insertions(+), 7 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/markdown.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 app/src/main/java/com/musheer360/swiftslate/manager/BlocklistManager.kt create mode 100644 app/src/main/java/com/musheer360/swiftslate/model/AppInfo.kt create mode 100644 app/src/main/java/com/musheer360/swiftslate/ui/BlocklistScreen.kt create mode 100644 gradle/gradle-daemon-jvm.properties 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/java/com/musheer360/swiftslate/manager/BlocklistManager.kt b/app/src/main/java/com/musheer360/swiftslate/manager/BlocklistManager.kt new file mode 100644 index 0000000..18c86db --- /dev/null +++ b/app/src/main/java/com/musheer360/swiftslate/manager/BlocklistManager.kt @@ -0,0 +1,27 @@ +package com.musheer360.swiftslate.manager + +import android.content.SharedPreferences + +object BlocklistManager { + private const val KEY = "blocked_packages" + + fun getBlocklist(prefs: SharedPreferences): Set { + 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/model/AppInfo.kt b/app/src/main/java/com/musheer360/swiftslate/model/AppInfo.kt new file mode 100644 index 0000000..b38c6b4 --- /dev/null +++ b/app/src/main/java/com/musheer360/swiftslate/model/AppInfo.kt @@ -0,0 +1,11 @@ +package com.musheer360.swiftslate.model + +import android.graphics.drawable.Drawable +import androidx.compose.runtime.Immutable + +@Immutable +data class AppInfo( + val name: String, + val packageName: String, + val icon: Drawable +) 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..6376f12 --- /dev/null +++ b/app/src/main/java/com/musheer360/swiftslate/ui/BlocklistScreen.kt @@ -0,0 +1,277 @@ +package com.musheer360.swiftslate.ui + +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.widget.ImageView +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +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.* +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.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.ui.components.SlateCard +import com.musheer360.swiftslate.ui.components.SlateItemCard +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun BlocklistScreen(prefs: SharedPreferences, 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)) } + + // Load apps asynchronously on launch + LaunchedEffect(Unit) { + withContext(Dispatchers.IO) { + val pm = context.packageManager + val apps = pm.getInstalledApplications(PackageManager.GET_META_DATA) + .filter { app -> + // Filter to only user-launchable apps to keep list clean and relevant + pm.getLaunchIntentForPackage(app.packageName) != null + } + .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, app.packageName) + } else { + BlocklistManager.removeApp(prefs, app.packageName) + } + blocklist = BlocklistManager.getBlocklist(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/SettingsScreen.kt b/app/src/main/java/com/musheer360/swiftslate/ui/SettingsScreen.kt index 96768ff..cd98988 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,14 @@ 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 @@ -42,6 +45,12 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) { 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) } @@ -148,12 +157,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 @@ -442,6 +454,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 +594,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 From 1baef2c96b0fe440b067bb8b2fe574484bd2905d Mon Sep 17 00:00:00 2001 From: firad Date: Sun, 24 May 2026 20:44:15 +0600 Subject: [PATCH 2/2] Fix Compose stability warnings and show all apps in blocklist --- app/src/main/AndroidManifest.xml | 1 + .../swiftslate/SwiftSlateViewModel.kt | 4 +- .../swiftslate/manager/CommandManager.kt | 2 + .../swiftslate/manager/KeyManager.kt | 2 + .../swiftslate/manager/StatsManager.kt | 2 + .../musheer360/swiftslate/model/AppInfo.kt | 6 ++ .../swiftslate/ui/BlocklistScreen.kt | 47 +++++++---- .../musheer360/swiftslate/ui/KeysScreen.kt | 81 ++++++++++++++----- .../swiftslate/ui/SettingsScreen.kt | 41 +++++----- 9 files changed, 133 insertions(+), 53 deletions(-) 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 @@ + Unit) { +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)) } + 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) - .filter { app -> - // Filter to only user-launchable apps to keep list clean and relevant - pm.getLaunchIntentForPackage(app.packageName) != null - } .map { app -> AppInfo( name = app.loadLabel(pm).toString(), @@ -255,11 +274,11 @@ fun BlocklistScreen(prefs: SharedPreferences, onBack: () -> Unit) { onCheckedChange = { checkState -> haptic.performHapticFeedback(HapticFeedbackType.LongPress) if (checkState) { - BlocklistManager.addApp(prefs, app.packageName) + BlocklistManager.addApp(prefs.prefs, app.packageName) } else { - BlocklistManager.removeApp(prefs, app.packageName) + BlocklistManager.removeApp(prefs.prefs, app.packageName) } - blocklist = BlocklistManager.getBlocklist(prefs) + blocklist = BlocklistManager.getBlocklist(prefs.prefs) }, colors = SwitchDefaults.colors( checkedThumbColor = MaterialTheme.colorScheme.primary, 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 cd98988..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,6 +1,5 @@ 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 @@ -31,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 @@ -40,7 +41,7 @@ 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 @@ -55,24 +56,24 @@ fun SettingsScreen(commandManager: CommandManager, prefs: SharedPreferences) { 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) @@ -88,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 @@ -101,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 } @@ -202,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 } ) @@ -211,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 } ) @@ -220,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 } ) @@ -257,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 } ) @@ -294,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 } ) @@ -327,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() } } }, @@ -357,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)) }, @@ -376,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 @@ -393,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,