From b5614258e3e4cf5a81e6a003e36a5478fa2546d8 Mon Sep 17 00:00:00 2001 From: KR Developer Date: Tue, 25 Aug 2026 04:34:12 +0530 Subject: [PATCH 1/3] fix: subtle layout shifts when expandable categories shrink --- .../essentials/ui/components/CategoryExpandableSection.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/CategoryExpandableSection.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/CategoryExpandableSection.kt index 34c8c5c42..520b3d340 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/CategoryExpandableSection.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/CategoryExpandableSection.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -60,7 +61,6 @@ fun CategoryExpandableSection( Column( modifier = modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp), ) { Row( modifier = @@ -70,7 +70,8 @@ fun CategoryExpandableSection( .clickable { HapticUtil.performUIHaptic(view) onToggleExpand() - }.padding(horizontal = 12.dp, vertical = 8.dp), + } + .padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { @@ -115,8 +116,9 @@ fun CategoryExpandableSection( visible = isExpanded, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut(), + modifier = Modifier.clip(RoundedCornerShape(24.dp)) ) { - RoundedCardContainer(spacing = 2.dp) { + RoundedCardContainer(modifier = Modifier.padding(top = 8.dp)) { content() } } From 86c62cd9a42dea51a5201700014272405dec4259 Mon Sep 17 00:00:00 2001 From: KR Developer Date: Tue, 25 Aug 2026 06:24:15 +0530 Subject: [PATCH 2/3] fix(settings): subtle layout shifts when expandable categories shrink (Permissions) --- .../ui/activities/SettingsActivity.kt | 467 ++++++++---------- .../components/CategoryExpandableSection.kt | 40 +- 2 files changed, 241 insertions(+), 266 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt index 748006a01..e71fd8336 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt @@ -91,6 +91,7 @@ import com.sameerasw.essentials.domain.registry.FeatureRegistry import com.sameerasw.essentials.domain.registry.PermissionRegistry import com.sameerasw.essentials.translation.TranslationManager import com.sameerasw.essentials.translation.ui.TranslationSessionSheet +import com.sameerasw.essentials.ui.components.CategoryExpandableSection import com.sameerasw.essentials.ui.components.EssentialsFloatingToolbar import com.sameerasw.essentials.ui.components.MadebySameeraswCard import com.sameerasw.essentials.ui.components.dialogs.AboutSection @@ -935,285 +936,255 @@ fun SettingsContent( Spacer(modifier = Modifier.height(16.dp)) - // Permissions Section - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable { isPermissionsExpanded = !isPermissionsExpanded } - .padding(start = 16.dp, top = 16.dp, bottom = 8.dp, end = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + // Expandable permissions + CategoryExpandableSection( + title = "Permissions", + isSettingsSection = true, + isExpanded = isPermissionsExpanded, + onToggleExpand = { isPermissionsExpanded = !isPermissionsExpanded }, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = "Permissions", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Icon( - painter = - painterResource( - id = if (isPermissionsExpanded) R.drawable.rounded_keyboard_arrow_up_24 else R.drawable.rounded_keyboard_arrow_down_24, - ), - contentDescription = if (isPermissionsExpanded) "Collapse" else "Expand", - tint = MaterialTheme.colorScheme.onSurfaceVariant, + PermissionCard( + iconRes = R.drawable.rounded_settings_accessibility_24, + title = "Accessibility", + dependentFeatures = PermissionRegistry.getFeatures("ACCESSIBILITY"), + actionLabel = if (isAccessibilityEnabled) "Granted" else "Grant Permission", + isGranted = isAccessibilityEnabled, + onActionClick = { + val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) + context.startActivity(intent) + }, ) - } - AnimatedVisibility( - visible = isPermissionsExpanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut(), - ) { - RoundedCardContainer { - PermissionCard( - iconRes = R.drawable.rounded_settings_accessibility_24, - title = "Accessibility", - dependentFeatures = PermissionRegistry.getFeatures("ACCESSIBILITY"), - actionLabel = if (isAccessibilityEnabled) "Granted" else "Grant Permission", - isGranted = isAccessibilityEnabled, - onActionClick = { - val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) - context.startActivity(intent) - }, - ) + PermissionCard( + iconRes = R.drawable.rounded_security_24, + title = "Write Secure Settings", + dependentFeatures = PermissionRegistry.getFeatures("WRITE_SECURE_SETTINGS"), + actionLabel = if (isWriteSecureSettingsEnabled) "Granted" else "Copy ADB Command", + isGranted = isWriteSecureSettingsEnabled, + onActionClick = { + val adbCommand = + "adb shell pm grant com.sameerasw.essentials android.permission.WRITE_SECURE_SETTINGS" + val clipboard = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("adb_command", adbCommand) + clipboard.setPrimaryClip(clip) + }, + secondaryActionLabel = "Check", + onSecondaryActionClick = { + viewModel.check(context) + }, + ) + if (isRootEnabled) { PermissionCard( - iconRes = R.drawable.rounded_security_24, - title = "Write Secure Settings", - dependentFeatures = PermissionRegistry.getFeatures("WRITE_SECURE_SETTINGS"), - actionLabel = if (isWriteSecureSettingsEnabled) "Granted" else "Copy ADB Command", - isGranted = isWriteSecureSettingsEnabled, + iconRes = R.drawable.rounded_numbers_24, + title = stringResource(R.string.perm_root_title), + dependentFeatures = PermissionRegistry.getFeatures("ROOT"), + actionLabel = if (isRootPermissionGranted) "Granted" else "Grant Access", + isGranted = isRootPermissionGranted, onActionClick = { - val adbCommand = - "adb shell pm grant com.sameerasw.essentials android.permission.WRITE_SECURE_SETTINGS" - val clipboard = - context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("adb_command", adbCommand) - clipboard.setPrimaryClip(clip) - }, - secondaryActionLabel = "Check", - onSecondaryActionClick = { viewModel.check(context) }, ) - - if (isRootEnabled) { - PermissionCard( - iconRes = R.drawable.rounded_numbers_24, - title = stringResource(R.string.perm_root_title), - dependentFeatures = PermissionRegistry.getFeatures("ROOT"), - actionLabel = if (isRootPermissionGranted) "Granted" else "Grant Access", - isGranted = isRootPermissionGranted, - onActionClick = { - viewModel.check(context) - }, - ) - } else if (isShizukuAvailable) { - PermissionCard( - iconRes = R.drawable.rounded_adb_24, - title = "Shizuku", - dependentFeatures = PermissionRegistry.getFeatures("SHIZUKU"), - actionLabel = if (isShizukuPermissionGranted) "Granted" else "Request Permission", - isGranted = isShizukuPermissionGranted, - onActionClick = { - viewModel.requestShizukuPermission() - }, - secondaryActionLabel = if (isShizukuPermissionGranted && !isWriteSecureSettingsEnabled) "Auto-Grant" else null, - onSecondaryActionClick = - if (isShizukuPermissionGranted && !isWriteSecureSettingsEnabled) { - { - viewModel.grantWriteSecureSettingsWithShizuku(context) - } - } else { - null - }, - ) - } - + } else if (isShizukuAvailable) { PermissionCard( - iconRes = R.drawable.rounded_android_cell_dual_4_bar_24, - title = "Read Phone State", - dependentFeatures = PermissionRegistry.getFeatures("READ_PHONE_STATE"), - actionLabel = if (isReadPhoneStateEnabled) "Granted" else "Grant Permission", - isGranted = isReadPhoneStateEnabled, + iconRes = R.drawable.rounded_adb_24, + title = "Shizuku", + dependentFeatures = PermissionRegistry.getFeatures("SHIZUKU"), + actionLabel = if (isShizukuPermissionGranted) "Granted" else "Request Permission", + isGranted = isShizukuPermissionGranted, onActionClick = { - viewModel.requestReadPhoneStatePermission(context as ComponentActivity) + viewModel.requestShizukuPermission() }, + secondaryActionLabel = if (isShizukuPermissionGranted && !isWriteSecureSettingsEnabled) "Auto-Grant" else null, + onSecondaryActionClick = + if (isShizukuPermissionGranted && !isWriteSecureSettingsEnabled) { + { + viewModel.grantWriteSecureSettingsWithShizuku(context) + } + } else { + null + }, ) + } - PermissionCard( - iconRes = R.drawable.rounded_notifications_unread_24, - title = "Post Notifications", - dependentFeatures = PermissionRegistry.getFeatures("POST_NOTIFICATIONS"), - actionLabel = if (isPostNotificationsEnabled) "Granted" else "Grant Permission", - isGranted = isPostNotificationsEnabled, - onActionClick = { - // Request permission - ActivityCompat.requestPermissions( - context as ComponentActivity, - arrayOf(Manifest.permission.POST_NOTIFICATIONS), - 1002, + PermissionCard( + iconRes = R.drawable.rounded_android_cell_dual_4_bar_24, + title = "Read Phone State", + dependentFeatures = PermissionRegistry.getFeatures("READ_PHONE_STATE"), + actionLabel = if (isReadPhoneStateEnabled) "Granted" else "Grant Permission", + isGranted = isReadPhoneStateEnabled, + onActionClick = { + viewModel.requestReadPhoneStatePermission(context as ComponentActivity) + }, + ) + + PermissionCard( + iconRes = R.drawable.rounded_notifications_unread_24, + title = "Post Notifications", + dependentFeatures = PermissionRegistry.getFeatures("POST_NOTIFICATIONS"), + actionLabel = if (isPostNotificationsEnabled) "Granted" else "Grant Permission", + isGranted = isPostNotificationsEnabled, + onActionClick = { + // Request permission + ActivityCompat.requestPermissions( + context as ComponentActivity, + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + 1002, + ) + }, + ) + + PermissionCard( + iconRes = R.drawable.rounded_magnify_fullscreen_24, + title = "Draw Overlays", + dependentFeatures = PermissionRegistry.getFeatures("DRAW_OVER_OTHER_APPS"), + actionLabel = if (isOverlayPermissionGranted) "Granted" else "Grant Permission", + isGranted = isOverlayPermissionGranted, + onActionClick = { + val intent = + Intent( + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + android.net.Uri.parse("package:${context.packageName}"), ) - }, - ) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + }, + ) - PermissionCard( - iconRes = R.drawable.rounded_magnify_fullscreen_24, - title = "Draw Overlays", - dependentFeatures = PermissionRegistry.getFeatures("DRAW_OVER_OTHER_APPS"), - actionLabel = if (isOverlayPermissionGranted) "Granted" else "Grant Permission", - isGranted = isOverlayPermissionGranted, - onActionClick = { - val intent = - Intent( - Settings.ACTION_MANAGE_OVERLAY_PERMISSION, - android.net.Uri.parse("package:${context.packageName}"), - ) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - context.startActivity(intent) - }, - ) + PermissionCard( + iconRes = R.drawable.rounded_notification_settings_24, + title = "Notification Listener", + dependentFeatures = PermissionRegistry.getFeatures("NOTIFICATION_LISTENER"), + actionLabel = if (isNotificationListenerEnabled) "Granted" else "Enable listener", + isGranted = isNotificationListenerEnabled, + onActionClick = { + val intent = + Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + context.startActivity(intent) + }, + ) - PermissionCard( - iconRes = R.drawable.rounded_notification_settings_24, - title = "Notification Listener", - dependentFeatures = PermissionRegistry.getFeatures("NOTIFICATION_LISTENER"), - actionLabel = if (isNotificationListenerEnabled) "Granted" else "Enable listener", - isGranted = isNotificationListenerEnabled, - onActionClick = { - val intent = - Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - context.startActivity(intent) - }, - ) + PermissionCard( + iconRes = R.drawable.rounded_security_24, + title = stringResource(R.string.perm_write_settings_title), + dependentFeatures = PermissionRegistry.getFeatures("WRITE_SETTINGS"), + actionLabel = if (isWriteSettingsEnabled) "Granted" else "Grant Permission", + isGranted = isWriteSettingsEnabled, + onActionClick = { + PermissionUtils.openWriteSettings(context) + }, + ) - PermissionCard( - iconRes = R.drawable.rounded_security_24, - title = stringResource(R.string.perm_write_settings_title), - dependentFeatures = PermissionRegistry.getFeatures("WRITE_SETTINGS"), - actionLabel = if (isWriteSettingsEnabled) "Granted" else "Grant Permission", - isGranted = isWriteSettingsEnabled, - onActionClick = { - PermissionUtils.openWriteSettings(context) - }, - ) + PermissionCard( + iconRes = R.drawable.rounded_volume_up_24, + title = stringResource(R.string.perm_notif_policy_title), + dependentFeatures = PermissionRegistry.getFeatures("NOTIFICATION_POLICY"), + actionLabel = if (isNotificationPolicyAccessGranted) "Granted" else "Grant Permission", + isGranted = isNotificationPolicyAccessGranted, + onActionClick = { + PermissionUtils.openNotificationPolicySettings(context) + }, + ) - PermissionCard( - iconRes = R.drawable.rounded_volume_up_24, - title = stringResource(R.string.perm_notif_policy_title), - dependentFeatures = PermissionRegistry.getFeatures("NOTIFICATION_POLICY"), - actionLabel = if (isNotificationPolicyAccessGranted) "Granted" else "Grant Permission", - isGranted = isNotificationPolicyAccessGranted, - onActionClick = { - PermissionUtils.openNotificationPolicySettings(context) - }, - ) + PermissionCard( + iconRes = R.drawable.rounded_open_in_browser_24, + title = stringResource(R.string.perm_default_browser_title), + dependentFeatures = PermissionRegistry.getFeatures("DEFAULT_BROWSER"), + actionLabel = if (isDefaultBrowserSet) "Granted" else "Set as Default", + isGranted = isDefaultBrowserSet, + onActionClick = { + val intent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS) + try { + context.startActivity(intent) + } catch (e: Exception) { + // Fallback for older Android versions + val settingsIntent = Intent(Settings.ACTION_SETTINGS) + context.startActivity(settingsIntent) + } + }, + ) - PermissionCard( - iconRes = R.drawable.rounded_open_in_browser_24, - title = stringResource(R.string.perm_default_browser_title), - dependentFeatures = PermissionRegistry.getFeatures("DEFAULT_BROWSER"), - actionLabel = if (isDefaultBrowserSet) "Granted" else "Set as Default", - isGranted = isDefaultBrowserSet, - onActionClick = { - val intent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS) - try { - context.startActivity(intent) - } catch (e: Exception) { - // Fallback for older Android versions - val settingsIntent = Intent(Settings.ACTION_SETTINGS) - context.startActivity(settingsIntent) - } - }, - ) + PermissionCard( + iconRes = R.drawable.rounded_settings_motion_mode_24, + title = stringResource(R.string.perm_write_settings_title), + dependentFeatures = PermissionRegistry.getFeatures("WRITE_SETTINGS"), + actionLabel = if (isWriteSettingsEnabled) "Granted" else "Grant Permission", + isGranted = isWriteSettingsEnabled, + onActionClick = { + PermissionUtils.openWriteSettings(context) + }, + ) - PermissionCard( - iconRes = R.drawable.rounded_settings_motion_mode_24, - title = stringResource(R.string.perm_write_settings_title), - dependentFeatures = PermissionRegistry.getFeatures("WRITE_SETTINGS"), - actionLabel = if (isWriteSettingsEnabled) "Granted" else "Grant Permission", - isGranted = isWriteSettingsEnabled, - onActionClick = { - PermissionUtils.openWriteSettings(context) - }, - ) + PermissionCard( + iconRes = R.drawable.rounded_notifications_off_24, + title = stringResource(R.string.perm_notif_policy_title), + dependentFeatures = PermissionRegistry.getFeatures("NOTIFICATION_POLICY"), + actionLabel = if (isNotificationPolicyAccessGranted) "Granted" else "Grant Permission", + isGranted = isNotificationPolicyAccessGranted, + onActionClick = { + PermissionUtils.openNotificationPolicySettings(context) + }, + ) - PermissionCard( - iconRes = R.drawable.rounded_notifications_off_24, - title = stringResource(R.string.perm_notif_policy_title), - dependentFeatures = PermissionRegistry.getFeatures("NOTIFICATION_POLICY"), - actionLabel = if (isNotificationPolicyAccessGranted) "Granted" else "Grant Permission", - isGranted = isNotificationPolicyAccessGranted, - onActionClick = { - PermissionUtils.openNotificationPolicySettings(context) - }, - ) + PermissionCard( + iconRes = R.drawable.rounded_data_usage_24, + title = stringResource(R.string.perm_usage_stats_title), + dependentFeatures = PermissionRegistry.getFeatures("USAGE_STATS"), + actionLabel = if (isUsageStatsPermissionGranted) "Granted" else "Grant Permission", + isGranted = isUsageStatsPermissionGranted, + onActionClick = { + PermissionUtils.openUsageStatsSettings(context) + }, + ) - PermissionCard( - iconRes = R.drawable.rounded_data_usage_24, - title = stringResource(R.string.perm_usage_stats_title), - dependentFeatures = PermissionRegistry.getFeatures("USAGE_STATS"), - actionLabel = if (isUsageStatsPermissionGranted) "Granted" else "Grant Permission", - isGranted = isUsageStatsPermissionGranted, - onActionClick = { - PermissionUtils.openUsageStatsSettings(context) - }, - ) + PermissionCard( + iconRes = R.drawable.rounded_location_on_24, + title = "Location Access", + dependentFeatures = PermissionRegistry.getFeatures("LOCATION"), + actionLabel = if (isLocationPermissionGranted) "Granted" else "Grant Permission", + isGranted = isLocationPermissionGranted, + onActionClick = { + viewModel.requestLocationPermission(context as ComponentActivity) + }, + ) + if (isLocationPermissionGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { PermissionCard( iconRes = R.drawable.rounded_location_on_24, - title = "Location Access", - dependentFeatures = PermissionRegistry.getFeatures("LOCATION"), - actionLabel = if (isLocationPermissionGranted) "Granted" else "Grant Permission", - isGranted = isLocationPermissionGranted, + title = "Background Location", + dependentFeatures = PermissionRegistry.getFeatures("BACKGROUND_LOCATION"), + actionLabel = if (isBackgroundLocationPermissionGranted) "Granted" else "Grant Permission", + isGranted = isBackgroundLocationPermissionGranted, onActionClick = { - viewModel.requestLocationPermission(context as ComponentActivity) + viewModel.requestBackgroundLocationPermission(context as ComponentActivity) }, ) + } - if (isLocationPermissionGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - PermissionCard( - iconRes = R.drawable.rounded_location_on_24, - title = "Background Location", - dependentFeatures = PermissionRegistry.getFeatures("BACKGROUND_LOCATION"), - actionLabel = if (isBackgroundLocationPermissionGranted) "Granted" else "Grant Permission", - isGranted = isBackgroundLocationPermissionGranted, - onActionClick = { - viewModel.requestBackgroundLocationPermission(context as ComponentActivity) - }, - ) - } - - PermissionCard( - iconRes = R.drawable.rounded_admin_panel_settings_24, - title = "Device Admin", - dependentFeatures = PermissionRegistry.getFeatures("DEVICE_ADMIN"), - actionLabel = if (isDeviceAdminEnabled) "Granted" else "Enable Admin", - isGranted = isDeviceAdminEnabled, - onActionClick = { - viewModel.requestDeviceAdmin(context) - }, - ) + PermissionCard( + iconRes = R.drawable.rounded_admin_panel_settings_24, + title = "Device Admin", + dependentFeatures = PermissionRegistry.getFeatures("DEVICE_ADMIN"), + actionLabel = if (isDeviceAdminEnabled) "Granted" else "Enable Admin", + isGranted = isDeviceAdminEnabled, + onActionClick = { + viewModel.requestDeviceAdmin(context) + }, + ) - PermissionCard( - iconRes = R.drawable.rounded_calendar_today_24, - title = "Calendar", - dependentFeatures = PermissionRegistry.getFeatures("READ_CALENDAR"), - actionLabel = if (isCalendarPermissionGranted) "Granted" else "Grant Permission", - isGranted = isCalendarPermissionGranted, - onActionClick = { - viewModel.requestCalendarPermission(context as ComponentActivity) - }, - ) - } + PermissionCard( + iconRes = R.drawable.rounded_calendar_today_24, + title = "Calendar", + dependentFeatures = PermissionRegistry.getFeatures("READ_CALENDAR"), + actionLabel = if (isCalendarPermissionGranted) "Granted" else "Grant Permission", + isGranted = isCalendarPermissionGranted, + onActionClick = { + viewModel.requestCalendarPermission(context as ComponentActivity) + }, + ) } Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/CategoryExpandableSection.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/CategoryExpandableSection.kt index 520b3d340..e22af7c1d 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/CategoryExpandableSection.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/CategoryExpandableSection.kt @@ -46,11 +46,12 @@ import com.sameerasw.essentials.utils.HapticUtil @Composable fun CategoryExpandableSection( + modifier: Modifier = Modifier, title: String, - itemCount: Int, + isSettingsSection: Boolean = false, + itemCount: Int? = null, isExpanded: Boolean, onToggleExpand: () -> Unit, - modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit, ) { val view = LocalView.current @@ -71,7 +72,7 @@ fun CategoryExpandableSection( HapticUtil.performUIHaptic(view) onToggleExpand() } - .padding(horizontal = 12.dp, vertical = 8.dp), + .padding(if (isSettingsSection) 16.dp else 12.dp, 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { @@ -82,22 +83,25 @@ fun CategoryExpandableSection( Text( text = title, style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, + fontWeight = if (!isSettingsSection) FontWeight.Bold else null, + color = if (isSettingsSection) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface, ) - Box( - modifier = - Modifier - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceContainerHighest) - .padding(horizontal = 8.dp, vertical = 2.dp), - ) { - Text( - text = itemCount.toString(), - style = MaterialTheme.typography.labelSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + + if (itemCount != null) { + Box( + modifier = + Modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(horizontal = 8.dp, vertical = 2.dp), + ) { + Text( + text = itemCount.toString(), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } } From cc0a13f7868bc052988331883445dd905488d557 Mon Sep 17 00:00:00 2001 From: KR Developer Date: Tue, 25 Aug 2026 13:22:59 +0530 Subject: [PATCH 3/3] feat: disable link preview setting for link actions sheet, --- .../data/repository/SettingsRepository.kt | 1 + .../ui/activities/FeatureSettingsActivity.kt | 14 +- .../ui/activities/LinkPickerActivity.kt | 2 + .../linkActions/LinkPickerAdapter.kt | 166 +++++++++--------- .../ui/features/apps/LinkActionsSettingsUI.kt | 92 ++++++++++ .../essentials/viewmodels/MainViewModel.kt | 13 ++ 6 files changed, 205 insertions(+), 83 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/features/apps/LinkActionsSettingsUI.kt diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt index f34b92c64..179c59d36 100644 --- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt @@ -387,6 +387,7 @@ class SettingsRepository( const val KEY_TRANSLATION_MODE_DO_NOT_SHOW_WARNING = "translation_mode_do_not_show_warning" const val KEY_LOCKDOWN_MODE = "lockdown_mode" + const val KEY_DISABLE_LINK_PREVIEW = "disable_link_preview" } /** diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt index a4e3bfe92..e6f321dac 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt @@ -62,6 +62,7 @@ import com.sameerasw.essentials.ui.components.linkActions.LinkPickerScreen import com.sameerasw.essentials.ui.core.cards.FeatureCard import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer import com.sameerasw.essentials.ui.core.sheets.PermissionsBottomSheet +import com.sameerasw.essentials.ui.features.apps.LinkActionsSettingsUI import com.sameerasw.essentials.ui.features.battery.BatteriesSettingsUI import com.sameerasw.essentials.ui.features.security.AppLockSettingsUI import com.sameerasw.essentials.ui.features.system.AlwaysOnDisplaySettingsUI @@ -142,8 +143,7 @@ class FeatureSettingsActivity : AppCompatActivity() { val featureObj = FeatureRegistry.ALL_FEATURES.find { it.id == featureId } val highlightSetting = intent.getStringExtra("highlight_setting") - if (featureId == "Link actions" || featureId == "URL Shortener") { - val isShortenerDirect = featureId == "URL Shortener" + if (featureId == "URL Shortener") { setContent { val viewModel: MainViewModel = viewModel() val context = LocalContext.current @@ -157,8 +157,8 @@ class FeatureSettingsActivity : AppCompatActivity() { onFinish = { finish() }, modifier = Modifier.fillMaxSize(), demo = false, - initialTab = if (isShortenerDirect) 2 else 0, - initialOpenShorten = isShortenerDirect, + initialTab = 2, + initialOpenShorten = true, ) } } @@ -1211,6 +1211,12 @@ class FeatureSettingsActivity : AppCompatActivity() { onShowMoveSheetChange = { isStandbyMoveSheetVisible = it }, ) } + + "Link actions" -> { + LinkActionsSettingsUI( + viewModel = viewModel + ) + } } } // Bottom padding for toolbar diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/LinkPickerActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/LinkPickerActivity.kt index 6e769b478..d9b6a482f 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/LinkPickerActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/LinkPickerActivity.kt @@ -19,6 +19,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import com.sameerasw.essentials.data.repository.SettingsRepository import com.sameerasw.essentials.ui.components.linkActions.LinkPickerScreen import com.sameerasw.essentials.ui.theme.EssentialsTheme @@ -75,6 +76,7 @@ class LinkPickerActivity : AppCompatActivity() { EssentialsTheme(pitchBlackTheme = isPitchBlackThemeEnabled) { LinkPickerScreen( uri = uri, + disableLinkPreview = SettingsRepository(context).getBoolean(SettingsRepository.KEY_DISABLE_LINK_PREVIEW), // something is wrong with viewmodel and maybe just loading one setting is better than loading whole viewmodel state... onFinish = { finish() }, modifier = Modifier.fillMaxSize(), ) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/linkActions/LinkPickerAdapter.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/linkActions/LinkPickerAdapter.kt index a9ffeb242..0a70b0178 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/linkActions/LinkPickerAdapter.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/linkActions/LinkPickerAdapter.kt @@ -20,7 +20,6 @@ import android.net.Uri import android.text.Html import android.util.Log import android.widget.Toast -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable @@ -32,20 +31,14 @@ 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.IntrinsicSize -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars -import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -58,15 +51,12 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledIconButton -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.ToggleButton @@ -87,12 +77,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource @@ -102,14 +90,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.content.edit -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween -import androidx.compose.animation.expandHorizontally -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkHorizontally import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -120,8 +103,6 @@ import com.sameerasw.essentials.R import com.sameerasw.essentials.data.repository.SettingsRepository import com.sameerasw.essentials.ui.modifiers.BlurDirection import com.sameerasw.essentials.ui.modifiers.progressiveBlur -import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer -import com.sameerasw.essentials.ui.core.pickers.SegmentedPicker import com.sameerasw.essentials.ui.core.sheets.EssentialsBottomSheet import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.utils.PermissionUtils @@ -129,7 +110,6 @@ import com.sameerasw.essentials.utils.WindowingUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.BufferedReader import java.io.InputStreamReader @@ -201,6 +181,7 @@ private fun cleanTrackingParams(uri: Uri): Uri { @Composable fun LinkPickerScreen( uri: Uri, + disableLinkPreview: Boolean = false, onFinish: () -> Unit, modifier: Modifier = Modifier, demo: Boolean = false, @@ -223,7 +204,7 @@ fun LinkPickerScreen( // Preview data state var linkPreviewData by remember { mutableStateOf(null) } - var isLoadingPreview by remember { mutableStateOf(true) } + var isLoadingPreview by remember { mutableStateOf(!disableLinkPreview) } // App lists var baseOpenWithApps by remember { mutableStateOf>(emptyList()) } @@ -234,7 +215,7 @@ fun LinkPickerScreen( LaunchedEffect(currentUri) { isLoadingApps = true - isLoadingPreview = true + isLoadingPreview = !disableLinkPreview linkPreviewData = null withContext(Dispatchers.IO) { @@ -252,7 +233,7 @@ fun LinkPickerScreen( } // Fetch preview data asynchronously and smoothly update when ready - val preview = fetchLinkPreviewData(currentUri) + val preview = if (!disableLinkPreview) fetchLinkPreviewData(currentUri) else null withContext(Dispatchers.Main) { linkPreviewData = preview isLoadingPreview = false @@ -451,66 +432,88 @@ fun LinkPickerScreen( ) { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween ) { - Box( - modifier = - Modifier - .size(40.dp) - .background( - MaterialTheme.colorScheme.primaryContainer, - RoundedCornerShape(12.dp), - ), - contentAlignment = Alignment.Center, + Row( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Crossfade( - targetState = if (isLoadingPreview) "loading" else (linkPreviewData?.faviconUrl ?: "icon"), - label = "FaviconCrossfade", - ) { state -> - if (state == "loading") { - LoadingIndicator( - modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } else if (state != "icon" && !linkPreviewData?.faviconUrl.isNullOrBlank()) { - AsyncImage( - model = - ImageRequest.Builder(context) - .data(linkPreviewData?.faviconUrl) - .crossfade(true) - .build(), - contentDescription = "Website Icon", - modifier = - Modifier - .size(24.dp) - .clip(RoundedCornerShape(6.dp)), - ) - } else { - Icon( - painter = painterResource(id = R.drawable.rounded_link_24), - contentDescription = "Link Icon", - modifier = Modifier.size(22.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer, - ) + Box( + modifier = + Modifier + .size(40.dp) + .background( + MaterialTheme.colorScheme.primaryContainer, + RoundedCornerShape(12.dp), + ), + contentAlignment = Alignment.Center, + ) { + Crossfade( + targetState = if (isLoadingPreview) "loading" else (linkPreviewData?.faviconUrl ?: "icon"), + label = "FaviconCrossfade", + ) { state -> + if (state == "loading") { + LoadingIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } else if (state != "icon" && !linkPreviewData?.faviconUrl.isNullOrBlank()) { + AsyncImage( + model = + ImageRequest.Builder(context) + .data(linkPreviewData?.faviconUrl) + .crossfade(true) + .build(), + contentDescription = "Website Icon", + modifier = + Modifier + .size(24.dp) + .clip(RoundedCornerShape(6.dp)), + ) + } else { + Icon( + painter = painterResource(id = R.drawable.rounded_link_24), + contentDescription = "Link Icon", + modifier = Modifier.size(22.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } } } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = domain, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = currentUri.toString(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } } - Column(modifier = Modifier.weight(1f)) { - Text( - text = domain, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = currentUri.toString(), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 2, - overflow = TextOverflow.Ellipsis, + // Save it for later + IconButton( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + } + ) { + Icon( + painter = painterResource(R.drawable.rounded_bookmark_24), + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurface, ) } } @@ -636,10 +639,14 @@ fun LinkPickerScreen( }, shape = RoundedCornerShape(16.dp), color = MaterialTheme.colorScheme.surfaceBright, - modifier = Modifier.fillMaxSize().maskClip(RoundedCornerShape(16.dp)), + modifier = Modifier + .fillMaxSize() + .maskClip(RoundedCornerShape(16.dp)), ) { Row( - modifier = Modifier.fillMaxSize().padding(horizontal = 10.dp), + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, ) { @@ -828,7 +835,8 @@ fun LinkPickerScreen( height = topBlurHeightPx, direction = BlurDirection.TOP, showGradientOverlay = false, - ).progressiveBlur( + ) + .progressiveBlur( blurRadius = 40f, height = bottomBlurHeightPx, direction = BlurDirection.BOTTOM, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/apps/LinkActionsSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/apps/LinkActionsSettingsUI.kt new file mode 100644 index 000000000..f8a3ceada --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/apps/LinkActionsSettingsUI.kt @@ -0,0 +1,92 @@ +package com.sameerasw.essentials.ui.features.apps + +/* + * Copyright (c) 2026 sameerasw.com + * License: MIT License + * + * Feature Module: Link Action Features + * File: LinkActionsSettingsUI.kt + * Description: Composable screen for Link actions settings. + */ + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.components.linkActions.LinkPickerScreen +import com.sameerasw.essentials.ui.core.cards.FeatureCard +import com.sameerasw.essentials.ui.core.cards.IconToggleItem +import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.modifiers.highlight +import com.sameerasw.essentials.viewmodels.MainViewModel + +@Composable +fun LinkActionsSettingsUI( + viewModel: MainViewModel, + modifier: Modifier = Modifier, + highlightSetting: String? = null, +) { + var linkActionVisible by remember { mutableStateOf(false) } + + if (linkActionVisible) { + LinkPickerScreen( + uri = "https://sameerasw.com".toUri(), + disableLinkPreview = viewModel.disableLinkPreview.value, + onFinish = { linkActionVisible = !linkActionVisible }, + modifier = Modifier.fillMaxSize(), + ) + } + + Column( + modifier = + modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "Link action options", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + RoundedCardContainer(spacing = 2.dp) { + FeatureCard( + modifier = Modifier.highlight(highlightSetting == "Link action"), + title = "Link action", + description = "Open link actions sheet", + iconRes = R.drawable.rounded_link_24, + isEnabled = true, + isToggleEnabled = false, + showToggle = false, + onDisabledToggleClick = null, + hasMoreSettings = false, + isBeta = false, + onToggle = {}, + onClick = { linkActionVisible = !linkActionVisible }, + ) + + IconToggleItem( + iconRes = R.drawable.rounded_window_open_24, + title = "Disable link previews", + description = "It helps protect your ip address if you don't want to visit the site.", + isChecked = viewModel.disableLinkPreview.value, + onCheckedChange = { viewModel.toggleDisableLinkPreview() }, + modifier = Modifier.highlight(highlightSetting == "disable_link_preview"), + ) + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index bf8513d6d..f253f04fe 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -379,6 +379,8 @@ class MainViewModel : ViewModel() { private var workflowPollingJob: kotlinx.coroutines.Job? = null val gitHubUser = mutableStateOf(null) + val disableLinkPreview = mutableStateOf(false) + private val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { override fun onChange( @@ -1106,6 +1108,15 @@ class MainViewModel : ViewModel() { } } + fun toggleDisableLinkPreview() { + val status = !disableLinkPreview.value + settingsRepository.putBoolean( + SettingsRepository.KEY_DISABLE_LINK_PREVIEW, + status + ) + disableLinkPreview.value = status + } + /** * Executes the check operation. * @@ -1117,6 +1128,8 @@ class MainViewModel : ViewModel() { updateRepository = UpdateRepository() gitHubUser.value = settingsRepository.getGitHubUser() + disableLinkPreview.value = settingsRepository.getBoolean(SettingsRepository.KEY_DISABLE_LINK_PREVIEW) + // Sync with system per-app language settings val currentLocales = AppCompatDelegate.getApplicationLocales() if (!currentLocales.isEmpty) {