From bedad745c322cb80aa482bdbb3832f4fa091b6b9 Mon Sep 17 00:00:00 2001 From: Isaac Serrano Date: Thu, 8 Oct 2026 04:15:31 -0600 Subject: [PATCH 1/4] feat(island): new option for compact line peek notification --- .../data/repository/SettingsRepository.kt | 4 + .../domain/registry/FeatureRegistry.kt | 5 ++ .../essentials/island/model/IslandModels.kt | 5 ++ .../notifications/NotificationsPlugin.kt | 10 +++ .../essentials/island/ui/IslandRoot.kt | 50 +++++++++---- .../essentials/island/ui/LineTemplate.kt | 74 +++++++++++++++++++ .../IslandNotificationOptionsBottomSheet.kt | 19 +++++ .../essentials/viewmodels/MainViewModel.kt | 10 +++ app/src/main/res/values/strings.xml | 2 + .../essentials/island/CondensedLineTest.kt | 54 ++++++++++++++ 10 files changed, 219 insertions(+), 14 deletions(-) create mode 100644 app/src/test/java/com/sameerasw/essentials/island/CondensedLineTest.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 dd113fc314..e51c9ca64d 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 @@ -618,6 +618,7 @@ class SettingsRepository( const val KEY_ISLAND_MEDIA_SHOW_PREVIOUS = "island_media_show_previous" const val KEY_ISLAND_MEDIA_SHOW_LIKE = "island_media_show_like" const val KEY_ISLAND_NOTIF_COMPACT_HEADS_UP = "island_notif_compact_heads_up" + const val KEY_ISLAND_NOTIF_CONDENSED_INFO = "island_notif_condensed_info" const val KEY_ISLAND_NOTIF_KEEP_PROGRESS = "island_notif_keep_progress" const val KEY_ISLAND_NOTIF_QUEUE = "island_notif_queue" const val KEY_ISLAND_NOTIF_SKIP_SILENT = "island_notif_skip_silent" @@ -4138,6 +4139,9 @@ class SettingsRepository( fun isIslandNotifCompactHeadsUpEnabled(): Boolean = getBoolean(KEY_ISLAND_NOTIF_COMPACT_HEADS_UP, true) fun setIslandNotifCompactHeadsUpEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_NOTIF_COMPACT_HEADS_UP, enabled) + fun isIslandNotifCondensedInfoEnabled(): Boolean = getBoolean(KEY_ISLAND_NOTIF_CONDENSED_INFO, false) + fun setIslandNotifCondensedInfoEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_NOTIF_CONDENSED_INFO, enabled) + fun isIslandNotifKeepProgressEnabled(): Boolean = getBoolean(KEY_ISLAND_NOTIF_KEEP_PROGRESS, true) fun setIslandNotifKeepProgressEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_NOTIF_KEEP_PROGRESS, enabled) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt index d333a3d72a..88bc184b2f 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt @@ -827,6 +827,11 @@ object FeatureRegistry { R.string.island_notif_compact_heads_up_desc, "island_notif_compact_heads_up", ), + SearchSetting( + R.string.island_notif_condensed_info_title, + R.string.island_notif_condensed_info_desc, + "island_notif_condensed_info", + ), SearchSetting( R.string.island_notif_skip_silent_title, R.string.island_notification_options_title, diff --git a/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt b/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt index 6e1a05fb14..e2cfa165d6 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt @@ -50,6 +50,8 @@ class LineContent( val start: String, val end: String, val endSlot: (@Composable () -> Unit)? = null, + // Icon + title only on line peek + val condensed: Boolean = false, ) interface IslandExpandedScope { @@ -122,6 +124,9 @@ class IslandItem( val sourcePackage: String? = null, // Temporary priority while the item's state is urgent; null falls back to `priority`. val priorityOverride: Int? = null, + // Identifies which piece of content the item is currently showing. The item key stays put while a plugin cycles + // through its own content, so this is what tells the island a genuinely new thing arrived and it should pulse. + val contentId: String? = null, val compactVisible: Boolean = true, val needsCompanyAtCenter: Boolean = false, diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt index 6b5f7aecdd..153468a0ae 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt @@ -37,6 +37,8 @@ class NotificationsPlugin : BaseIslandPlugin() { override val settingKeys = setOf( SettingsRepository.KEY_ISLAND_CATCH_UP_ENABLED, SettingsRepository.KEY_ISLAND_NOTIF_COMPACT_HEADS_UP, + SettingsRepository.KEY_ISLAND_NOTIF_CONDENSED_INFO, + SettingsRepository.KEY_ISLAND_LINE_STAGE_ENABLED, SettingsRepository.KEY_ISLAND_SHOW_GLOW, SettingsRepository.KEY_ISLAND_NOTIF_QUEUE, SettingsRepository.KEY_ISLAND_NOTIF_SKIP_SILENT, @@ -333,6 +335,9 @@ class NotificationsPlugin : BaseIslandPlugin() { private fun queueEnabled() = settings.isIslandNotifQueueEnabled() + private fun condensedLine() = settings.isIslandNotifCondensedInfoEnabled() && + settings.isIslandLineStageEnabled() && settings.isIslandNotifCompactHeadsUpEnabled() + private fun itemFor(alert: ActiveNotificationAlert): IslandItem { if (concealed()) return concealedItemFor(alert) val (sender, message) = senderAndMessage(context, alert) @@ -359,6 +364,7 @@ class NotificationsPlugin : BaseIslandPlugin() { icon = { IslandBitmap(icon, 24.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) }, start = sender, end = message, + condensed = condensedLine(), ), expanded = ExpandedContent { scope -> NotificationExpanded( @@ -383,6 +389,7 @@ class NotificationsPlugin : BaseIslandPlugin() { popCurrent(reExpand = false) }, sourcePackage = alert.packageName, + contentId = alert.key, ) } @@ -414,6 +421,7 @@ class NotificationsPlugin : BaseIslandPlugin() { icon = { IslandBitmap(icon, 24.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) }, start = appNameFor(context, alert), end = "", + condensed = condensedLine(), ), accent = alert.appColor?.let { Color(soften(it)) }, dismissible = true, @@ -424,6 +432,7 @@ class NotificationsPlugin : BaseIslandPlugin() { onOpen = open, interactions = InteractionOverrides(onTap = { open(); true }), sourcePackage = alert.packageName, + contentId = alert.key, ) } @@ -448,6 +457,7 @@ class NotificationsPlugin : BaseIslandPlugin() { interactions = interactions, queue = queue, sourcePackage = sourcePackage, + contentId = contentId, stack = stack, compactVisible = sideBubble == null, sideBubble = sideBubble, diff --git a/app/src/main/java/com/sameerasw/essentials/island/ui/IslandRoot.kt b/app/src/main/java/com/sameerasw/essentials/island/ui/IslandRoot.kt index 851fdaf8b7..f731b2da17 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/ui/IslandRoot.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/ui/IslandRoot.kt @@ -203,7 +203,7 @@ fun IslandRoot( animateColorAsState(dynamicOutline ?: base, label = "islandOutlineColor").value } val pulse = remember { Animatable(0f) } - LaunchedEffect(state.focusedKey, spec.pulseShadow) { + LaunchedEffect(state.focusedKey, state.items[state.focusedKey]?.contentId, spec.pulseShadow) { pulse.snapTo(0f) if (!spec.pulseShadow || state.focusedKey == null) return@LaunchedEffect pulse.animateTo(1f, tween(250, easing = LinearEasing)) @@ -407,11 +407,17 @@ fun IslandRoot( if (spec.growDirection == 0 && hasCompactCells) catchUpAnim.value.coerceAtLeast(0f) * (bubbleSizePx + bubbleGapPx) / 2f else 0f val lineShift = remember { Animatable(0f) } val liveSurfaceHeight = remember { mutableIntStateOf(0) } - fun surfaceLeft(width: Int): Int = windowWidth / 2 + lineShift.value.roundToInt() + catchShiftPx().roundToInt() - when { - spec.growDirection > 0 -> cameraSlotPx / 2 - spec.growDirection < 0 -> width - cameraSlotPx / 2 - else -> width / 2 + val condensedLead = remember { Animatable(0f) } + val condensedLeadPx = with(density) { + (spec.cameraGap + spec.compactHeight * OPTICAL_INSET_RATIO + spec.cellSize).roundToPx() } + fun condensedShift(width: Int): Int = (width - cameraSlotPx) / 2 - condensedLeadPx + fun surfaceShiftFor(width: Int): Int = when { + spec.growDirection != 0 -> spec.growDirection * (width - cameraSlotPx) / 2 + else -> lerp(0, condensedShift(width), condensedLead.value) + } + fun surfaceLeft(width: Int): Int = windowWidth / 2 - width / 2 + surfaceShiftFor(width) + + lineShift.value.roundToInt() + catchShiftPx().roundToInt() fun bubbleX(width: Int): Int = if (spec.growDirection > 0) surfaceLeft(width) + width + bubbleGapPx else surfaceLeft(width) - bubbleGapPx - bubbleSizePx val focusedItem = state.focused @@ -430,8 +436,16 @@ fun IslandRoot( val lineInsetActive = queueShown && stage == IslandStage.Line val lineInset = if (lineInsetActive) spec.compactHeight + spec.cameraGap else 0.dp val lineInsets = if (spec.growDirection > 0) 0.dp to lineInset else lineInset to 0.dp - LaunchedEffect(lineInsetActive, spec.growDirection) { - val shift = if (lineInsetActive && spec.growDirection == 0) (bubbleSizePx + bubbleGapPx) / 2f else 0f + val condensedLineActive = spec.growDirection == 0 && stage == IslandStage.Line && focusedItem?.line?.condensed == true + LaunchedEffect(condensedLineActive) { + condensedLead.animateTo(if (condensedLineActive) 1f else 0f, IslandMotion.float()) + } + LaunchedEffect(lineInsetActive, spec.growDirection, condensedLineActive) { + val shift = if (lineInsetActive && spec.growDirection == 0 && !condensedLineActive) { + (bubbleSizePx + bubbleGapPx) / 2f + } else { + 0f + } lineShift.animateTo(shift, IslandMotion.float()) } var lastQueuedIcons by remember { mutableStateOf(emptyList()) } @@ -456,15 +470,23 @@ fun IslandRoot( } } - LaunchedEffect(target, windowWidth, stage, queueShown, pillSize, lineInsetActive, catchUpShown, hasCompactCells, bubbleOnlyLayout) { + LaunchedEffect( + target, windowWidth, stage, queueShown, pillSize, lineInsetActive, catchUpShown, hasCompactCells, + bubbleOnlyLayout, condensedLineActive, + ) { if (target == IntSize.Zero || windowWidth == 0) return@LaunchedEffect val g = if (stage == IslandStage.Expanded) outsetPx.roundToInt() else 0 - val shift = if ((lineInsetActive || (catchUpShown && hasCompactCells)) && spec.growDirection == 0) (bubbleSizePx + bubbleGapPx) / 2 else 0 - val left = windowWidth / 2 + shift - when { - spec.growDirection > 0 -> cameraSlotPx / 2 - spec.growDirection < 0 -> target.width - cameraSlotPx / 2 - else -> target.width / 2 + val shift = if ((lineInsetActive || (catchUpShown && hasCompactCells)) && spec.growDirection == 0 && !condensedLineActive) { + (bubbleSizePx + bubbleGapPx) / 2 + } else { + 0 + } + val settled = when { + spec.growDirection != 0 -> spec.growDirection * (target.width - cameraSlotPx) / 2 + condensedLineActive -> condensedShift(target.width) + else -> 0 } + val left = windowWidth / 2 - target.width / 2 + settled + shift val top = (surfaceTopPx - g).coerceAtLeast(0) var bounds = IntRect(left, top, left + target.width, surfaceTopPx - g + target.height) if (queueShown && stage == IslandStage.Line) { @@ -626,7 +648,7 @@ fun IslandRoot( .padding(top = spec.surfaceTop) .layout { measurable, constraints -> val p = measurable.measure(constraints) - val dx = spec.growDirection * (p.width - cameraSlotPx) / 2 + lineShift.value.roundToInt() + catchShiftPx().roundToInt() + val dx = surfaceShiftFor(p.width) + lineShift.value.roundToInt() + catchShiftPx().roundToInt() layout(p.width, p.height) { p.place(dx, 0) } } .offset { IntOffset(0, (squashPx / 2f * squash.value - edgeShift.floatValue - surfaceTopPx * bond).roundToInt()) } diff --git a/app/src/main/java/com/sameerasw/essentials/island/ui/LineTemplate.kt b/app/src/main/java/com/sameerasw/essentials/island/ui/LineTemplate.kt index c4b316d407..2c4db90c64 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/ui/LineTemplate.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/ui/LineTemplate.kt @@ -14,8 +14,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontVariation @@ -29,6 +32,10 @@ import com.sameerasw.essentials.island.model.LineContent @Composable fun LineTemplate(line: LineContent, spec: IslandLayoutSpec, startInset: Dp = 0.dp, endInset: Dp = 0.dp) { + if (line.condensed) { + CondensedLine(line, spec, startInset, endInset) + return + } val textEndInset = if (line.endSlot == null && spec.growDirection >= 0) spec.compactHeight * OPTICAL_INSET_RATIO else 0.dp Row( modifier = Modifier @@ -78,6 +85,73 @@ fun LineTemplate(line: LineContent, spec: IslandLayoutSpec, startInset: Dp = 0.d } } +internal fun condensedTitleWidth( + natural: Dp, + lineWidth: Dp, + cameraSlotWidth: Dp, + cellSize: Dp, + edgePad: Dp, + endInset: Dp, +): Dp { + val roomBesideCamera = ((lineWidth - edgePad * 2 - cameraSlotWidth - endInset) / 2).coerceAtLeast(cellSize) + return natural.coerceIn(cellSize, roomBesideCamera) +} + +@Composable +private fun CondensedLine(line: LineContent, spec: IslandLayoutSpec, startInset: Dp, endInset: Dp) { + val style = IslandTextStyles.line.copy(fontWeight = FontWeight.SemiBold) + val measurer = rememberTextMeasurer() + val naturalPx = remember(line.start, style, measurer) { + measurer.measure(line.start, style, maxLines = 1, softWrap = false).size.width + } + val natural = with(LocalDensity.current) { (naturalPx + 1).toDp() } + val icon: @Composable () -> Unit = { + Box(Modifier.squareFit(spec.cellSize), contentAlignment = Alignment.Center) { line.icon() } + } + val title: @Composable (Dp) -> Unit = { width -> + MarqueeText(text = line.start, style = style, textAlign = TextAlign.Start, modifier = Modifier.width(width)) + } + + if (spec.growDirection == 0) { + val edgePad = spec.cameraGap + spec.compactHeight * OPTICAL_INSET_RATIO + Row( + modifier = Modifier + .height(spec.compactHeight) + .padding(horizontal = edgePad), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(Modifier.width(spec.cellSize), contentAlignment = Alignment.Center) { icon() } + Spacer(Modifier.width(spec.cameraSlotWidth)) + title( + condensedTitleWidth( + natural = natural, + lineWidth = spec.lineWidth, + cameraSlotWidth = spec.cameraSlotWidth, + cellSize = spec.cellSize, + edgePad = edgePad, + endInset = endInset, + ), + ) + } + } else { + val opticalInset = if (spec.growDirection > 0) spec.compactHeight * OPTICAL_INSET_RATIO else 0.dp + val fixed = spec.cameraGap + opticalInset + spec.cameraSlotWidth + spec.cellSize + spec.cellSpacing * 2 + val maxText = (spec.lineWidth - startInset - endInset - fixed).coerceAtLeast(0.dp) + Row( + modifier = Modifier + .height(spec.compactHeight) + .padding(start = spec.cameraGap, end = spec.cameraGap + opticalInset), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(spec.cellSpacing), + ) { + if (spec.growDirection > 0) Spacer(Modifier.width(spec.cameraSlotWidth - spec.cameraGap)) + icon() + title(natural.coerceAtMost(maxText)) + if (spec.growDirection < 0) Spacer(Modifier.width(spec.cameraSlotWidth - spec.cameraGap)) + } + } +} + @Composable private fun LineText(text: String, align: TextAlign, weight: FontWeight, modifier: Modifier) { MarqueeText(text = text, style = IslandTextStyles.line.copy(fontWeight = weight), textAlign = align, modifier = modifier) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationOptionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationOptionsBottomSheet.kt index d1eb6411ee..455095b8ed 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationOptionsBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationOptionsBottomSheet.kt @@ -137,6 +137,25 @@ fun IslandNotificationOptionsBottomSheet( ) } + // Only reachable once a notification actually peeks as a line, which needs both toggles above. + AnimatedVisibility( + visible = viewModel.isIslandLineStageEnabled.value && viewModel.isIslandNotifCompactHeadsUp.value, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + IconToggleItem( + iconRes = R.drawable.rounded_view_headline_24, + title = stringResource(R.string.island_notif_condensed_info_title), + description = stringResource(R.string.island_notif_condensed_info_desc), + isChecked = viewModel.isIslandNotifCondensedInfo.value, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandNotifCondensedInfo(checked) + }, + modifier = Modifier.highlight(highlightSetting == "island_notif_condensed_info"), + ) + } + IconToggleItem( iconRes = R.drawable.rounded_downloading_24, title = stringResource(R.string.island_notif_keep_progress_title), 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 e377239b7d..679b0ecb9c 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -252,6 +252,7 @@ class MainViewModel : ViewModel() { val isIslandMediaShowPrevious = mutableStateOf(false) val isIslandMediaShowLike = mutableStateOf(true) val isIslandNotifCompactHeadsUp = mutableStateOf(true) + val isIslandNotifCondensedInfo = mutableStateOf(false) val isIslandNotifKeepProgress = mutableStateOf(true) val isIslandNotifQueue = mutableStateOf(true) val isIslandNotifSkipSilent = mutableStateOf(true) @@ -905,6 +906,9 @@ class MainViewModel : ViewModel() { SettingsRepository.KEY_ISLAND_NOTIF_COMPACT_HEADS_UP -> isIslandNotifCompactHeadsUp.value = settingsRepository.isIslandNotifCompactHeadsUpEnabled() + SettingsRepository.KEY_ISLAND_NOTIF_CONDENSED_INFO -> + isIslandNotifCondensedInfo.value = settingsRepository.isIslandNotifCondensedInfoEnabled() + SettingsRepository.KEY_ISLAND_NOTIF_QUEUE -> isIslandNotifQueue.value = settingsRepository.isIslandNotifQueueEnabled() @@ -2410,6 +2414,7 @@ class MainViewModel : ViewModel() { isIslandMediaShowPrevious.value = settingsRepository.isIslandMediaShowPreviousEnabled() isIslandMediaShowLike.value = settingsRepository.isIslandMediaShowLikeEnabled() isIslandNotifCompactHeadsUp.value = settingsRepository.isIslandNotifCompactHeadsUpEnabled() + isIslandNotifCondensedInfo.value = settingsRepository.isIslandNotifCondensedInfoEnabled() isIslandNotifKeepProgress.value = settingsRepository.isIslandNotifKeepProgressEnabled() isIslandNotifQueue.value = settingsRepository.isIslandNotifQueueEnabled() isIslandNotifSkipSilent.value = settingsRepository.isIslandNotifSkipSilentEnabled() @@ -5709,6 +5714,11 @@ class MainViewModel : ViewModel() { settingsRepository.setIslandNotifCompactHeadsUpEnabled(enabled) } + fun setIslandNotifCondensedInfo(enabled: Boolean) { + isIslandNotifCondensedInfo.value = enabled + settingsRepository.setIslandNotifCondensedInfoEnabled(enabled) + } + fun setIslandCatchUpEnabled(enabled: Boolean) { isIslandCatchUpEnabled.value = enabled settingsRepository.setIslandCatchUpEnabled(enabled) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9a5a15db63..39bcefac9b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3180,6 +3180,8 @@ Filter apps Compact heads up Show new notifications as a line peek + Compact info + Line peek shows only the app icon and the title. Tap to expand for the content and actions Peek song change Keep in catchup when paused Show previous track button diff --git a/app/src/test/java/com/sameerasw/essentials/island/CondensedLineTest.kt b/app/src/test/java/com/sameerasw/essentials/island/CondensedLineTest.kt new file mode 100644 index 0000000000..b6d2864c37 --- /dev/null +++ b/app/src/test/java/com/sameerasw/essentials/island/CondensedLineTest.kt @@ -0,0 +1,54 @@ +package com.sameerasw.essentials.island + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.island.ui.condensedTitleWidth +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class CondensedLineTest { + private val lineWidth = 360.dp + private val cameraSlot = 40.dp + private val cellSize = 28.dp + private val edgePad = 12.dp + + private fun title(natural: Dp, endInset: Dp = 0.dp) = + condensedTitleWidth(natural, lineWidth, cameraSlot, cellSize, edgePad, endInset) + + private fun pillWidth(natural: Dp, endInset: Dp = 0.dp) = + edgePad * 2 + cellSize + cameraSlot + title(natural, endInset) + + private fun leadIn() = edgePad + cellSize + + @Test fun shortTitleHugsTheText() { + assertEquals(60.dp, title(natural = 60.dp)) + assertEquals(152.dp, pillWidth(natural = 60.dp)) + } + + @Test fun longTitleStopsAtTheRoomBesideTheCamera() = assertEquals(148.dp, title(natural = 900.dp)) + + @Test fun titleNeverNarrowerThanTheIconBlock() = assertEquals(cellSize, title(natural = 4.dp)) + + @Test fun iconSideNeverGrowsWithTheTitle() { + val lead = leadIn() + listOf(0.dp, 60.dp, 900.dp).forEach { natural -> + assertEquals("natural=$natural", lead, pillWidth(natural) - title(natural) - cameraSlot - edgePad) + } + } + + @Test fun rightEdgeStaysInsideHalfTheMaxWidth() { + listOf(0.dp, 60.dp, 900.dp).forEach { natural -> + listOf(0.dp, 46.dp).forEach { endInset -> + val fromCentre = cameraSlot / 2 + edgePad + title(natural, endInset) + val budget = (lineWidth - endInset) / 2 + assertTrue("natural=$natural endInset=$endInset gave $fromCentre", fromCentre <= budget) + } + } + } + + @Test fun titleGrowsMonotonicallyWithItsText() { + val widths = listOf(0.dp, 30.dp, 60.dp, 140.dp, 900.dp).map { title(it) } + assertEquals(widths.sortedBy { it.value }, widths) + } +} From ddd21091f2135344d492d182be757d90460355d8 Mon Sep 17 00:00:00 2001 From: Isaac Serrano Date: Sun, 11 Oct 2026 18:39:10 -0700 Subject: [PATCH 2/4] refactor(island): move notification options to dedicated screen and center condensed line layout - Extract notification settings into a dedicated sub-page (`IslandNotificationSettingsUI`) and register `ISLAND_NOTIFICATIONS_FEATURE_ID`, replacing `IslandNotificationOptionsBottomSheet`. - Balance condensed line layout on both sides of the camera slot in `LineTemplate` and `IslandRoot`. - Update string resources and add unit tests in `CondensedLineTest` for centered camera positioning. --- .../domain/registry/FeatureRegistry.kt | 87 +++-- .../essentials/island/ui/IslandRoot.kt | 20 +- .../essentials/island/ui/LineTemplate.kt | 35 +- .../ui/activities/FeatureSettingsActivity.kt | 10 + .../ui/features/display/IslandSettingsUI.kt | 33 +- .../features/display/IslandSubSettingsUI.kt | 307 +++++++++++++++++- .../IslandNotificationOptionsBottomSheet.kt | 282 ---------------- app/src/main/res/values/strings.xml | 20 +- .../essentials/island/CondensedLineTest.kt | 18 +- 9 files changed, 435 insertions(+), 377 deletions(-) delete mode 100644 app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationOptionsBottomSheet.kt diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt index 88bc184b2f..db36bb3b7f 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt @@ -11,6 +11,7 @@ package com.sameerasw.essentials.domain.registry import com.sameerasw.essentials.ui.features.display.ISLAND_BEHAVIOR_FEATURE_ID import com.sameerasw.essentials.ui.features.display.ISLAND_DURATION_FEATURE_ID +import com.sameerasw.essentials.ui.features.display.ISLAND_NOTIFICATIONS_FEATURE_ID import com.sameerasw.essentials.ui.features.display.ISLAND_PLACEMENT_FEATURE_ID import com.sameerasw.essentials.ui.features.display.ISLAND_VISUALS_FEATURE_ID import android.content.Context @@ -817,31 +818,6 @@ object FeatureRegistry { R.string.island_call_incoming, "island_show_calls", ), - SearchSetting( - R.string.island_notif_queue_title, - R.string.island_notif_queue_title, - "island_notif_queue", - ), - SearchSetting( - R.string.island_notif_compact_heads_up_title, - R.string.island_notif_compact_heads_up_desc, - "island_notif_compact_heads_up", - ), - SearchSetting( - R.string.island_notif_condensed_info_title, - R.string.island_notif_condensed_info_desc, - "island_notif_condensed_info", - ), - SearchSetting( - R.string.island_notif_skip_silent_title, - R.string.island_notification_options_title, - "island_notif_skip_silent", - ), - SearchSetting( - R.string.island_notif_filter_apps_title, - R.string.island_notification_options_title, - "island_notif_filter_apps", - ), SearchSetting( R.string.island_media_peek_song_change_title, R.string.island_media_peek_song_change_desc, @@ -979,6 +955,67 @@ object FeatureRegistry { enabled: Boolean, ) {} }, + object : Feature( + id = ISLAND_NOTIFICATIONS_FEATURE_ID, + title = R.string.island_section_notifications, + iconRes = R.drawable.rounded_notifications_unread_24, + category = R.string.cat_interface, + description = R.string.island_notification_options_title, + showToggle = false, + parentFeatureId = "Island", + isVisibleInMain = false, + searchableSettings = + listOf( + SearchSetting( + R.string.island_notif_filter_apps_title, + R.string.island_notif_filter_apps_desc, + "island_notif_filter_apps", + ), + SearchSetting( + R.string.island_notif_skip_silent_title, + R.string.island_notif_skip_silent_desc, + "island_notif_skip_silent", + ), + SearchSetting( + R.string.island_notif_keep_progress_title, + R.string.island_notif_keep_progress_desc, + "island_notif_keep_progress", + ), + SearchSetting( + R.string.island_notif_compact_heads_up_title, + R.string.island_notif_compact_heads_up_desc, + "island_notif_compact_heads_up", + ), + SearchSetting( + R.string.island_notif_condensed_info_title, + R.string.island_notif_condensed_info_desc, + "island_notif_condensed_info", + ), + SearchSetting( + R.string.island_notif_queue_title, + R.string.island_notif_queue_desc, + "island_notif_queue", + ), + SearchSetting( + R.string.island_notif_tap_to_open_title, + R.string.island_notif_tap_to_open_desc, + "island_notif_tap_to_open", + ), + SearchSetting( + R.string.island_catch_up_title, + R.string.island_catch_up_desc, + "island_catch_up", + ), + ), + ) { + override fun isEnabled(viewModel: MainViewModel) = viewModel.isIslandEnabled.value + + override fun onToggle( + viewModel: MainViewModel, + context: Context, + enabled: Boolean, + ) {} + }, object : Feature( id = "Maps power saving mode", title = R.string.feat_maps_power_saving_title, diff --git a/app/src/main/java/com/sameerasw/essentials/island/ui/IslandRoot.kt b/app/src/main/java/com/sameerasw/essentials/island/ui/IslandRoot.kt index f731b2da17..69957cec1c 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/ui/IslandRoot.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/ui/IslandRoot.kt @@ -407,15 +407,7 @@ fun IslandRoot( if (spec.growDirection == 0 && hasCompactCells) catchUpAnim.value.coerceAtLeast(0f) * (bubbleSizePx + bubbleGapPx) / 2f else 0f val lineShift = remember { Animatable(0f) } val liveSurfaceHeight = remember { mutableIntStateOf(0) } - val condensedLead = remember { Animatable(0f) } - val condensedLeadPx = with(density) { - (spec.cameraGap + spec.compactHeight * OPTICAL_INSET_RATIO + spec.cellSize).roundToPx() - } - fun condensedShift(width: Int): Int = (width - cameraSlotPx) / 2 - condensedLeadPx - fun surfaceShiftFor(width: Int): Int = when { - spec.growDirection != 0 -> spec.growDirection * (width - cameraSlotPx) / 2 - else -> lerp(0, condensedShift(width), condensedLead.value) - } + fun surfaceShiftFor(width: Int): Int = spec.growDirection * (width - cameraSlotPx) / 2 fun surfaceLeft(width: Int): Int = windowWidth / 2 - width / 2 + surfaceShiftFor(width) + lineShift.value.roundToInt() + catchShiftPx().roundToInt() fun bubbleX(width: Int): Int = @@ -437,9 +429,6 @@ fun IslandRoot( val lineInset = if (lineInsetActive) spec.compactHeight + spec.cameraGap else 0.dp val lineInsets = if (spec.growDirection > 0) 0.dp to lineInset else lineInset to 0.dp val condensedLineActive = spec.growDirection == 0 && stage == IslandStage.Line && focusedItem?.line?.condensed == true - LaunchedEffect(condensedLineActive) { - condensedLead.animateTo(if (condensedLineActive) 1f else 0f, IslandMotion.float()) - } LaunchedEffect(lineInsetActive, spec.growDirection, condensedLineActive) { val shift = if (lineInsetActive && spec.growDirection == 0 && !condensedLineActive) { (bubbleSizePx + bubbleGapPx) / 2f @@ -481,12 +470,7 @@ fun IslandRoot( } else { 0 } - val settled = when { - spec.growDirection != 0 -> spec.growDirection * (target.width - cameraSlotPx) / 2 - condensedLineActive -> condensedShift(target.width) - else -> 0 - } - val left = windowWidth / 2 - target.width / 2 + settled + shift + val left = windowWidth / 2 - target.width / 2 + surfaceShiftFor(target.width) + shift val top = (surfaceTopPx - g).coerceAtLeast(0) var bounds = IntRect(left, top, left + target.width, surfaceTopPx - g + target.height) if (queueShown && stage == IslandStage.Line) { diff --git a/app/src/main/java/com/sameerasw/essentials/island/ui/LineTemplate.kt b/app/src/main/java/com/sameerasw/essentials/island/ui/LineTemplate.kt index 2c4db90c64..09c3c11ace 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/ui/LineTemplate.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/ui/LineTemplate.kt @@ -114,24 +114,31 @@ private fun CondensedLine(line: LineContent, spec: IslandLayoutSpec, startInset: if (spec.growDirection == 0) { val edgePad = spec.cameraGap + spec.compactHeight * OPTICAL_INSET_RATIO + val titleWidth = condensedTitleWidth( + natural = natural, + lineWidth = spec.lineWidth, + cameraSlotWidth = spec.cameraSlotWidth, + cellSize = spec.cellSize, + edgePad = edgePad, + endInset = endInset, + ) + // Both halves get the same width so the camera slot stays dead centre and the pill reads balanced; the slack + // collects beside the camera while icon and title hug the outer ends, the rule CompactTemplate already uses. + // condensedTitleWidth floors at cellSize, so the icon half is never squeezed. + val side = titleWidth + edgePad Row( - modifier = Modifier - .height(spec.compactHeight) - .padding(horizontal = edgePad), + modifier = Modifier.height(spec.compactHeight), verticalAlignment = Alignment.CenterVertically, ) { - Box(Modifier.width(spec.cellSize), contentAlignment = Alignment.Center) { icon() } + Box( + modifier = Modifier.width(side).padding(start = edgePad), + contentAlignment = Alignment.CenterStart, + ) { icon() } Spacer(Modifier.width(spec.cameraSlotWidth)) - title( - condensedTitleWidth( - natural = natural, - lineWidth = spec.lineWidth, - cameraSlotWidth = spec.cameraSlotWidth, - cellSize = spec.cellSize, - edgePad = edgePad, - endInset = endInset, - ), - ) + Box( + modifier = Modifier.width(side).padding(end = edgePad), + contentAlignment = Alignment.CenterEnd, + ) { title(titleWidth) } } } else { val opticalInset = if (spec.growDirection > 0) spec.compactHeight * OPTICAL_INSET_RATIO else 0.dp 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 9440fc3d4a..4478773f2d 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 @@ -80,11 +80,13 @@ import com.sameerasw.essentials.ui.features.display.AodWallpaperSettingsUI import com.sameerasw.essentials.ui.features.display.DuoSettingsUI import com.sameerasw.essentials.ui.features.display.ISLAND_BEHAVIOR_FEATURE_ID import com.sameerasw.essentials.ui.features.display.ISLAND_DURATION_FEATURE_ID +import com.sameerasw.essentials.ui.features.display.ISLAND_NOTIFICATIONS_FEATURE_ID import com.sameerasw.essentials.ui.features.display.ISLAND_PLACEMENT_FEATURE_ID import com.sameerasw.essentials.ui.features.display.ISLAND_SUB_PAGE_IDS import com.sameerasw.essentials.ui.features.display.ISLAND_VISUALS_FEATURE_ID import com.sameerasw.essentials.ui.features.display.IslandBehaviorSettingsUI import com.sameerasw.essentials.ui.features.display.IslandDurationSettingsUI +import com.sameerasw.essentials.ui.features.display.IslandNotificationSettingsUI import com.sameerasw.essentials.ui.features.display.IslandPlacementSettingsUI import com.sameerasw.essentials.ui.features.display.IslandSettingsUI import com.sameerasw.essentials.ui.features.display.IslandVisualsSettingsUI @@ -1304,6 +1306,14 @@ class FeatureSettingsActivity : AppCompatActivity() { ) } + ISLAND_NOTIFICATIONS_FEATURE_ID -> { + IslandNotificationSettingsUI( + viewModel = viewModel, + modifier = Modifier.padding(top = 16.dp), + highlightSetting = highlightSetting, + ) + } + "Status glance" -> { StatusGlanceSettingsUI( viewModel = viewModel, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt index c6db675515..47e4e47952 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt @@ -73,7 +73,6 @@ import com.sameerasw.essentials.ui.features.display.sheets.IslandBorderOutlineOp import com.sameerasw.essentials.ui.features.display.sheets.IslandBriefOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandCallOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandDevicesBatteryBottomSheet -import com.sameerasw.essentials.ui.features.display.sheets.IslandNotificationOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandPulseShadowOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandSoundModeOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandTimeBatteryOptionsBottomSheet @@ -89,18 +88,6 @@ import com.sameerasw.essentials.utils.PermissionUtils import com.sameerasw.essentials.utils.ShellUtils import com.sameerasw.essentials.viewmodels.MainViewModel -private val notificationSheetSettings = - setOf( - "island_notif_compact_heads_up", - "island_notif_skip_silent", - "island_notif_filter_apps", - "island_notif_keep_progress", - "island_notif_queue", - "island_notif_tap_to_open", - "island_catch_up", - "island_catch_up_timeout", - ) - @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun IslandSettingsUI( @@ -131,9 +118,6 @@ fun IslandSettingsUI( var showSignalOptionsSheet by remember { mutableStateOf(false) } var showAlarmOptionsSheet by remember { mutableStateOf(false) } var showBriefOptionsSheet by remember { mutableStateOf(highlightSetting == "island_brief_show_alarm") } - var showNotificationOptionsSheet by remember { - mutableStateOf(highlightSetting in notificationSheetSettings) - } var showWeatherOptionsSheet by remember { mutableStateOf(false) } var pickingGesture by remember { mutableStateOf(null) } var showSlideModeSheet by remember { mutableStateOf(false) } @@ -281,7 +265,14 @@ fun IslandSettingsUI( HapticUtil.performVirtualKeyHaptic(view) viewModel.setIslandShowNotifications(checked) }, - onSettingsClick = { showNotificationOptionsSheet = true }, + onSettingsClick = { + HapticUtil.performVirtualKeyHaptic(view) + context.startActivity( + Intent(context, FeatureSettingsActivity::class.java).apply { + putExtra("feature", ISLAND_NOTIFICATIONS_FEATURE_ID) + }, + ) + }, modifier = Modifier.highlight(highlightSetting == "island_show_notifications"), ) "time_battery" -> IconToggleItem( @@ -760,14 +751,6 @@ fun IslandSettingsUI( ) } - if (showNotificationOptionsSheet) { - IslandNotificationOptionsBottomSheet( - viewModel = viewModel, - onDismissRequest = { showNotificationOptionsSheet = false }, - highlightSetting = highlightSetting, - ) - } - if (showBriefOptionsSheet) { IslandBriefOptionsBottomSheet( viewModel = viewModel, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt index 53b5de1b5b..fce38d7e8d 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt @@ -59,12 +59,13 @@ import com.sameerasw.essentials.ui.features.display.sheets.IslandBorderOutlineOp import com.sameerasw.essentials.ui.features.display.sheets.IslandBriefOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandCallOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandDevicesBatteryBottomSheet -import com.sameerasw.essentials.ui.features.display.sheets.IslandNotificationOptionsBottomSheet +import com.sameerasw.essentials.ui.features.display.sheets.IslandPrefToggle import com.sameerasw.essentials.ui.features.display.sheets.IslandPulseShadowOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandTimeBatteryOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandTimerOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandWeatherOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.StatusGlanceCalendarOptionsBottomSheet +import com.sameerasw.essentials.ui.features.display.sheets.rememberIslandShowsWhileLocked import com.sameerasw.essentials.ui.modifiers.highlight import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.utils.PermissionUIHelper @@ -76,8 +77,15 @@ const val ISLAND_PLACEMENT_FEATURE_ID = "Island placement" const val ISLAND_VISUALS_FEATURE_ID = "Island visuals" const val ISLAND_DURATION_FEATURE_ID = "Island duration" const val ISLAND_BEHAVIOR_FEATURE_ID = "Island behavior" +const val ISLAND_NOTIFICATIONS_FEATURE_ID = "Island notifications" -val ISLAND_SUB_PAGE_IDS = setOf(ISLAND_PLACEMENT_FEATURE_ID, ISLAND_VISUALS_FEATURE_ID, ISLAND_DURATION_FEATURE_ID, ISLAND_BEHAVIOR_FEATURE_ID) +val ISLAND_SUB_PAGE_IDS = setOf( + ISLAND_PLACEMENT_FEATURE_ID, + ISLAND_VISUALS_FEATURE_ID, + ISLAND_DURATION_FEATURE_ID, + ISLAND_BEHAVIOR_FEATURE_ID, + ISLAND_NOTIFICATIONS_FEATURE_ID, +) internal val ISLAND_SUB_PAGES = listOf( Triple(ISLAND_PLACEMENT_FEATURE_ID, R.string.island_section_placement, R.drawable.rounded_center_focus_strong_24), @@ -817,3 +825,298 @@ fun IslandPreviewControls(modifier: Modifier = Modifier) { } } } + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun IslandNotificationSettingsUI( + viewModel: MainViewModel, + modifier: Modifier = Modifier, + highlightSetting: String? = null, +) { + val context = LocalContext.current + val view = LocalView.current + var showFilterAppsSheet by remember { mutableStateOf(false) } + var concealed by remember { + mutableStateOf( + SettingsRepository(context).getBoolean(SettingsRepository.KEY_ISLAND_NOTIF_CONCEAL_LOCKED, false) + ) + } + // A notification can only peek as a line while Behavior's island-wide Line peek is on. + val linePeekOn = viewModel.isIslandLineStageEnabled.value + val compactHeadsUpOn = linePeekOn && viewModel.isIslandNotifCompactHeadsUp.value + + Column( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + RoundedCardContainer( + spacing = 2.dp, + cornerRadius = 24.dp, + ) { + IconToggleItem( + iconRes = R.drawable.rounded_notifications_unread_24, + title = stringResource(R.string.island_notif_show_title), + isChecked = viewModel.isIslandShowNotifications.value, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandShowNotifications(checked) + }, + modifier = Modifier.highlight(highlightSetting == "island_show_notifications"), + ) + } + + IslandSettingsSection(R.string.island_notif_section_sources) { + IconToggleItem( + iconRes = R.drawable.rounded_filter_alt_24, + title = stringResource(R.string.island_notif_filter_apps_title), + description = stringResource(R.string.island_notif_filter_apps_desc), + isChecked = viewModel.isIslandNotifFilterApps.value, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandNotifFilterApps(checked) + }, + modifier = Modifier.highlight(highlightSetting == "island_notif_filter_apps"), + ) + + AnimatedVisibility( + visible = viewModel.isIslandNotifFilterApps.value, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + IconToggleItem( + iconRes = R.drawable.rounded_apps_24, + title = stringResource(R.string.action_select_apps), + showToggle = false, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + showFilterAppsSheet = true + }, + ) + } + + IconToggleItem( + iconRes = R.drawable.rounded_notifications_off_24, + title = stringResource(R.string.island_notif_skip_silent_title), + description = stringResource(R.string.island_notif_skip_silent_desc), + isChecked = viewModel.isIslandNotifSkipSilent.value, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandNotifSkipSilent(checked) + }, + modifier = Modifier.highlight(highlightSetting == "island_notif_skip_silent"), + ) + + IconToggleItem( + iconRes = R.drawable.rounded_downloading_24, + title = stringResource(R.string.island_notif_keep_progress_title), + description = stringResource(R.string.island_notif_keep_progress_desc), + isChecked = viewModel.isIslandNotifKeepProgress.value, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandNotifKeepProgress(checked) + }, + modifier = Modifier.highlight(highlightSetting == "island_notif_keep_progress"), + ) + } + + IslandSettingsSection(R.string.island_notif_section_heads_up) { + IconToggleItem( + iconRes = R.drawable.rounded_notification_sound_24, + title = stringResource(R.string.island_notif_compact_heads_up_title), + description = stringResource( + if (linePeekOn) R.string.island_notif_compact_heads_up_desc + else R.string.island_notif_compact_heads_up_needs_line_peek, + ), + isChecked = compactHeadsUpOn, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandNotifCompactHeadsUp(checked) + }, + enabled = linePeekOn, + onDisabledClick = { + context.startActivity( + Intent(context, FeatureSettingsActivity::class.java).apply { + putExtra("feature", ISLAND_BEHAVIOR_FEATURE_ID) + putExtra("highlight_setting", "island_line_stage_enabled") + }, + ) + }, + modifier = Modifier.highlight(highlightSetting == "island_notif_compact_heads_up"), + ) + + AnimatedVisibility( + visible = compactHeadsUpOn, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + val condensed = viewModel.isIslandNotifCondensedInfo.value + val compactLabel = stringResource(R.string.island_notif_peek_style_compact) + val fullLabel = stringResource(R.string.island_notif_peek_style_full) + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceBright, MaterialTheme.shapes.extraSmall) + .padding(top = 12.dp) + .highlight(highlightSetting == "island_notif_condensed_info"), + ) { + Text( + text = stringResource(R.string.island_notif_condensed_info_title), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(horizontal = 16.dp), + ) + Text( + text = stringResource( + if (condensed) R.string.island_notif_condensed_info_desc + else R.string.island_notif_peek_style_full_desc, + ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp), + ) + SegmentedPicker( + items = listOf(true, false), + selectedItem = condensed, + onItemSelected = { viewModel.setIslandNotifCondensedInfo(it) }, + labelProvider = { if (it) compactLabel else fullLabel }, + title = R.string.island_notif_condensed_info_title, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + + IslandSettingsSection(R.string.island_notif_section_in_island) { + IconToggleItem( + iconRes = R.drawable.outline_circle_notifications_24, + title = stringResource(R.string.island_notif_queue_title), + description = stringResource(R.string.island_notif_queue_desc), + isChecked = viewModel.isIslandNotifQueue.value, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandNotifQueue(checked) + }, + modifier = Modifier.highlight(highlightSetting == "island_notif_queue"), + ) + + IconToggleItem( + iconRes = R.drawable.rounded_touch_app_24, + title = stringResource(R.string.island_notif_tap_to_open_title), + description = stringResource(R.string.island_notif_tap_to_open_desc), + isChecked = viewModel.isIslandNotifTapToOpen.value, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandNotifTapToOpen(checked) + }, + modifier = Modifier.highlight(highlightSetting == "island_notif_tap_to_open"), + ) + + IconToggleItem( + iconRes = R.drawable.rounded_notifications_unread_24, + title = stringResource(R.string.island_catch_up_title), + description = stringResource(R.string.island_catch_up_desc), + isChecked = viewModel.isIslandCatchUpEnabled.value, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandCatchUpEnabled(checked) + }, + modifier = Modifier.highlight(highlightSetting == "island_catch_up"), + ) + + AnimatedVisibility( + visible = viewModel.isIslandCatchUpEnabled.value, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + val infinityText = stringResource(R.string.island_timeout_infinity) + ConfigSliderItem( + title = stringResource(R.string.island_catch_up_timeout_title), + value = catchUpStepIndex(viewModel.islandCatchUpTimeoutMs.longValue).toFloat(), + onValueChange = { + HapticUtil.performUIHaptic(view) + viewModel.setIslandCatchUpTimeoutMs( + CATCH_UP_STEPS_MS[it.toInt().coerceIn(CATCH_UP_STEPS_MS.indices)] + ) + }, + valueRange = 0f..CATCH_UP_STEPS_MS.lastIndex.toFloat(), + increment = 1f, + iconRes = R.drawable.rounded_timer_24, + valueFormatter = { + formatCatchUp( + CATCH_UP_STEPS_MS[it.toInt().coerceIn(CATCH_UP_STEPS_MS.indices)], + infinityText, + ) + }, + modifier = Modifier.highlight(highlightSetting == "island_catch_up_timeout"), + ) + } + } + + if (rememberIslandShowsWhileLocked()) { + IslandSettingsSection(R.string.island_notif_section_lock_screen) { + IslandPrefToggle( + settingKey = SettingsRepository.KEY_ISLAND_NOTIF_CONCEAL_LOCKED, + iconRes = R.drawable.rounded_visibility_off_24, + title = stringResource(R.string.island_notif_conceal_locked_title), + onChanged = { concealed = it }, + ) + AnimatedVisibility( + visible = concealed, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + IslandPrefToggle( + settingKey = SettingsRepository.KEY_ISLAND_NOTIF_CONCEAL_CHAT_PICTURES, + iconRes = R.drawable.rounded_person_24, + title = stringResource(R.string.island_notif_conceal_chat_pictures_title), + ) + } + } + } + } + + if (showFilterAppsSheet) { + AppSelectionSheet( + title = stringResource(R.string.island_notif_filter_apps_title), + onDismissRequest = { showFilterAppsSheet = false }, + onLoadApps = { viewModel.loadIslandNotifFilterApps() }, + onSaveApps = { _, apps -> viewModel.saveIslandNotifFilterApps(apps) }, + onAppToggle = { _, pkg, enabled -> viewModel.updateIslandNotifFilterApp(pkg, enabled) }, + context = context, + ) + } +} + +// Header sits closer to its own card than to the previous section, so each group reads as one unit. +@Composable +private fun IslandSettingsSection(titleRes: Int, content: @Composable ColumnScope.() -> Unit) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = stringResource(titleRes), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 8.dp), + ) + RoundedCardContainer( + spacing = 2.dp, + cornerRadius = 24.dp, + content = content, + ) + } +} + +private val CATCH_UP_STEPS_MS = + longArrayOf(5_000, 10_000, 15_000, 20_000, 30_000, 45_000, 60_000, 120_000, 180_000, 300_000, 600_000, 900_000, 0) + +private fun catchUpStepIndex(ms: Long): Int = + if (ms <= 0L) CATCH_UP_STEPS_MS.lastIndex + else CATCH_UP_STEPS_MS.indices.filter { CATCH_UP_STEPS_MS[it] > 0 }.minBy { kotlin.math.abs(CATCH_UP_STEPS_MS[it] - ms) } + +private fun formatCatchUp(ms: Long, infinityText: String): String = + when { + ms <= 0L -> infinityText + ms < 60_000 -> "${ms / 1000}s" + else -> "${ms / 60_000}m" + } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationOptionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationOptionsBottomSheet.kt deleted file mode 100644 index 455095b8ed..0000000000 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationOptionsBottomSheet.kt +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright (c) 2026 sameerasw.com - * License: MIT License - * - * Feature Module: UI Feature - Display - * File: IslandNotificationOptionsBottomSheet.kt - * Description: Bottom sheet for configuring Island notification options. - */ - -package com.sameerasw.essentials.ui.features.display.sheets - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api -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.platform.LocalContext -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.sameerasw.essentials.R -import com.sameerasw.essentials.data.repository.SettingsRepository -import com.sameerasw.essentials.ui.components.sliders.ConfigSliderItem -import com.sameerasw.essentials.ui.core.cards.IconToggleItem -import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer -import com.sameerasw.essentials.ui.core.sheets.AppSelectionSheet -import com.sameerasw.essentials.ui.core.sheets.EssentialsBottomSheet -import com.sameerasw.essentials.ui.modifiers.highlight -import com.sameerasw.essentials.utils.HapticUtil -import com.sameerasw.essentials.viewmodels.MainViewModel - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun IslandNotificationOptionsBottomSheet( - viewModel: MainViewModel, - onDismissRequest: () -> Unit, - highlightSetting: String? = null, -) { - val view = LocalView.current - val context = LocalContext.current - var showFilterAppsSheet by remember { mutableStateOf(false) } - var concealed by remember { mutableStateOf(SettingsRepository(context).getBoolean(SettingsRepository.KEY_ISLAND_NOTIF_CONCEAL_LOCKED, false)) } - - EssentialsBottomSheet( - onDismissRequest = onDismissRequest, - ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(bottom = 24.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Text( - text = stringResource(R.string.island_notification_options_title), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(start = 8.dp, bottom = 4.dp), - ) - - RoundedCardContainer( - spacing = 2.dp, - cornerRadius = 24.dp, - ) { - IconToggleItem( - iconRes = R.drawable.rounded_filter_alt_24, - title = stringResource(R.string.island_notif_filter_apps_title), - isChecked = viewModel.isIslandNotifFilterApps.value, - onCheckedChange = { checked -> - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setIslandNotifFilterApps(checked) - }, - modifier = Modifier.highlight(highlightSetting == "island_notif_filter_apps"), - ) - - AnimatedVisibility( - visible = viewModel.isIslandNotifFilterApps.value, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - IconToggleItem( - iconRes = R.drawable.rounded_apps_24, - title = stringResource(R.string.action_select_apps), - showToggle = false, - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - showFilterAppsSheet = true - }, - ) - } - - IconToggleItem( - iconRes = R.drawable.rounded_notifications_off_24, - title = stringResource(R.string.island_notif_skip_silent_title), - isChecked = viewModel.isIslandNotifSkipSilent.value, - onCheckedChange = { checked -> - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setIslandNotifSkipSilent(checked) - }, - modifier = Modifier.highlight(highlightSetting == "island_notif_skip_silent"), - ) - } - - RoundedCardContainer( - spacing = 2.dp, - cornerRadius = 24.dp, - ) { - AnimatedVisibility( - visible = viewModel.isIslandLineStageEnabled.value, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - IconToggleItem( - iconRes = R.drawable.rounded_notification_sound_24, - title = stringResource(R.string.island_notif_compact_heads_up_title), - description = stringResource(R.string.island_notif_compact_heads_up_desc), - isChecked = viewModel.isIslandNotifCompactHeadsUp.value, - onCheckedChange = { checked -> - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setIslandNotifCompactHeadsUp(checked) - }, - modifier = Modifier.highlight(highlightSetting == "island_notif_compact_heads_up"), - ) - } - - // Only reachable once a notification actually peeks as a line, which needs both toggles above. - AnimatedVisibility( - visible = viewModel.isIslandLineStageEnabled.value && viewModel.isIslandNotifCompactHeadsUp.value, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - IconToggleItem( - iconRes = R.drawable.rounded_view_headline_24, - title = stringResource(R.string.island_notif_condensed_info_title), - description = stringResource(R.string.island_notif_condensed_info_desc), - isChecked = viewModel.isIslandNotifCondensedInfo.value, - onCheckedChange = { checked -> - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setIslandNotifCondensedInfo(checked) - }, - modifier = Modifier.highlight(highlightSetting == "island_notif_condensed_info"), - ) - } - - IconToggleItem( - iconRes = R.drawable.rounded_downloading_24, - title = stringResource(R.string.island_notif_keep_progress_title), - isChecked = viewModel.isIslandNotifKeepProgress.value, - onCheckedChange = { checked -> - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setIslandNotifKeepProgress(checked) - }, - modifier = Modifier.highlight(highlightSetting == "island_notif_keep_progress"), - ) - - IconToggleItem( - iconRes = R.drawable.outline_circle_notifications_24, - title = stringResource(R.string.island_notif_queue_title), - isChecked = viewModel.isIslandNotifQueue.value, - onCheckedChange = { checked -> - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setIslandNotifQueue(checked) - }, - modifier = Modifier.highlight(highlightSetting == "island_notif_queue"), - ) - - IconToggleItem( - iconRes = R.drawable.rounded_touch_app_24, - title = stringResource(R.string.island_notif_tap_to_open_title), - isChecked = viewModel.isIslandNotifTapToOpen.value, - onCheckedChange = { checked -> - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setIslandNotifTapToOpen(checked) - }, - modifier = Modifier.highlight(highlightSetting == "island_notif_tap_to_open"), - ) - } - - RoundedCardContainer( - spacing = 2.dp, - cornerRadius = 24.dp, - ) { - IconToggleItem( - iconRes = R.drawable.rounded_notifications_unread_24, - title = stringResource(R.string.island_catch_up_title), - description = stringResource(R.string.island_catch_up_desc), - isChecked = viewModel.isIslandCatchUpEnabled.value, - onCheckedChange = { checked -> - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setIslandCatchUpEnabled(checked) - }, - modifier = Modifier.highlight(highlightSetting == "island_catch_up"), - ) - - AnimatedVisibility( - visible = viewModel.isIslandCatchUpEnabled.value, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - val infinityText = stringResource(R.string.island_timeout_infinity) - ConfigSliderItem( - title = stringResource(R.string.island_catch_up_timeout_title), - value = catchUpStepIndex(viewModel.islandCatchUpTimeoutMs.longValue).toFloat(), - onValueChange = { - HapticUtil.performUIHaptic(view) - viewModel.setIslandCatchUpTimeoutMs(CATCH_UP_STEPS_MS[it.toInt().coerceIn(CATCH_UP_STEPS_MS.indices)]) - }, - valueRange = 0f..CATCH_UP_STEPS_MS.lastIndex.toFloat(), - increment = 1f, - iconRes = R.drawable.rounded_timer_24, - valueFormatter = { formatCatchUp(CATCH_UP_STEPS_MS[it.toInt().coerceIn(CATCH_UP_STEPS_MS.indices)], infinityText) }, - modifier = Modifier.highlight(highlightSetting == "island_catch_up_timeout"), - ) - } - } - - if (rememberIslandShowsWhileLocked()) { - RoundedCardContainer( - spacing = 2.dp, - cornerRadius = 24.dp, - ) { - IslandPrefToggle( - settingKey = SettingsRepository.KEY_ISLAND_NOTIF_CONCEAL_LOCKED, - iconRes = R.drawable.rounded_visibility_off_24, - title = stringResource(R.string.island_notif_conceal_locked_title), - onChanged = { concealed = it }, - ) - AnimatedVisibility( - visible = concealed, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - IslandPrefToggle( - settingKey = SettingsRepository.KEY_ISLAND_NOTIF_CONCEAL_CHAT_PICTURES, - iconRes = R.drawable.rounded_person_24, - title = stringResource(R.string.island_notif_conceal_chat_pictures_title), - ) - } - } - } - } - } - - if (showFilterAppsSheet) { - AppSelectionSheet( - title = stringResource(R.string.island_notif_filter_apps_title), - onDismissRequest = { showFilterAppsSheet = false }, - onLoadApps = { viewModel.loadIslandNotifFilterApps() }, - onSaveApps = { _, apps -> viewModel.saveIslandNotifFilterApps(apps) }, - onAppToggle = { _, pkg, enabled -> viewModel.updateIslandNotifFilterApp(pkg, enabled) }, - context = context, - ) - } -} - -private val CATCH_UP_STEPS_MS = - longArrayOf(5_000, 10_000, 15_000, 20_000, 30_000, 45_000, 60_000, 120_000, 180_000, 300_000, 600_000, 900_000, 0) - -private fun catchUpStepIndex(ms: Long): Int = - if (ms <= 0L) CATCH_UP_STEPS_MS.lastIndex - else CATCH_UP_STEPS_MS.indices.filter { CATCH_UP_STEPS_MS[it] > 0 }.minBy { kotlin.math.abs(CATCH_UP_STEPS_MS[it] - ms) } - -private fun formatCatchUp(ms: Long, infinityText: String): String = - when { - ms <= 0L -> infinityText - ms < 60_000 -> "${ms / 1000}s" - else -> "${ms / 60_000}m" - } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 39bcefac9b..5aed9f06c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3179,9 +3179,23 @@ Skip silent notifications Filter apps Compact heads up - Show new notifications as a line peek - Compact info - Line peek shows only the app icon and the title. Tap to expand for the content and actions + Peek new notifications as a line instead of expanding the island + Needs Line peek. Tap to turn it on in Behavior + Peek style + App icon and title only. Tap to expand for the content and actions + Compact + Full + Title on the left, message on the right + Show notifications + Which notifications + Heads up + In the island + Lock screen + Only show notifications from the apps you pick + Ignore notifications that arrive without sound + Show downloads and other ongoing progress + Keep several notifications instead of replacing the current one + Tap an expanded notification to open its app Peek song change Keep in catchup when paused Show previous track button diff --git a/app/src/test/java/com/sameerasw/essentials/island/CondensedLineTest.kt b/app/src/test/java/com/sameerasw/essentials/island/CondensedLineTest.kt index b6d2864c37..db7a6d9209 100644 --- a/app/src/test/java/com/sameerasw/essentials/island/CondensedLineTest.kt +++ b/app/src/test/java/com/sameerasw/essentials/island/CondensedLineTest.kt @@ -16,27 +16,29 @@ class CondensedLineTest { private fun title(natural: Dp, endInset: Dp = 0.dp) = condensedTitleWidth(natural, lineWidth, cameraSlot, cellSize, edgePad, endInset) - private fun pillWidth(natural: Dp, endInset: Dp = 0.dp) = - edgePad * 2 + cellSize + cameraSlot + title(natural, endInset) + // Mirrors CondensedLine: each half is the title width plus the edge padding, with the camera slot between them. + private fun side(natural: Dp, endInset: Dp = 0.dp) = title(natural, endInset) + edgePad - private fun leadIn() = edgePad + cellSize + private fun pillWidth(natural: Dp, endInset: Dp = 0.dp) = side(natural, endInset) * 2 + cameraSlot @Test fun shortTitleHugsTheText() { assertEquals(60.dp, title(natural = 60.dp)) - assertEquals(152.dp, pillWidth(natural = 60.dp)) + assertEquals(184.dp, pillWidth(natural = 60.dp)) } @Test fun longTitleStopsAtTheRoomBesideTheCamera() = assertEquals(148.dp, title(natural = 900.dp)) @Test fun titleNeverNarrowerThanTheIconBlock() = assertEquals(cellSize, title(natural = 4.dp)) - @Test fun iconSideNeverGrowsWithTheTitle() { - val lead = leadIn() - listOf(0.dp, 60.dp, 900.dp).forEach { natural -> - assertEquals("natural=$natural", lead, pillWidth(natural) - title(natural) - cameraSlot - edgePad) + @Test fun cameraSlotStaysAtThePillCentre() { + listOf(0.dp, 60.dp, 140.dp, 900.dp).forEach { natural -> + val slotStart = side(natural) + assertEquals("natural=$natural", pillWidth(natural) - slotStart - cameraSlot, slotStart) } } + @Test fun longestTitleStillFitsTheLineWidth() = assertTrue(pillWidth(natural = 900.dp) <= lineWidth) + @Test fun rightEdgeStaysInsideHalfTheMaxWidth() { listOf(0.dp, 60.dp, 900.dp).forEach { natural -> listOf(0.dp, 46.dp).forEach { endInset -> From 7cc50d80fb959f298564acbd54820789c2c3c135 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 13 Oct 2026 11:45:50 +0530 Subject: [PATCH 3/4] refactor: simplify notification peek settings UI and clean up code comments and unused strings --- .../essentials/island/model/IslandModels.kt | 3 +- .../essentials/island/ui/LineTemplate.kt | 3 -- .../features/display/IslandSubSettingsUI.kt | 37 ++++--------------- app/src/main/res/values/strings.xml | 2 - 4 files changed, 8 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt b/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt index 6f26dc92f2..ee2e056fb1 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt @@ -125,8 +125,7 @@ class IslandItem( val sourcePackage: String? = null, // Temporary priority while the item's state is urgent; null falls back to `priority`. val priorityOverride: Int? = null, - // Identifies which piece of content the item is currently showing. The item key stays put while a plugin cycles - // through its own content, so this is what tells the island a genuinely new thing arrived and it should pulse. + // Identifies the content currently shown, so a new one pulses even when the item key is unchanged. val contentId: String? = null, val compactVisible: Boolean = true, diff --git a/app/src/main/java/com/sameerasw/essentials/island/ui/LineTemplate.kt b/app/src/main/java/com/sameerasw/essentials/island/ui/LineTemplate.kt index e0aca6f626..c321cccc0a 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/ui/LineTemplate.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/ui/LineTemplate.kt @@ -122,9 +122,6 @@ private fun CondensedLine(line: LineContent, spec: IslandLayoutSpec, startInset: edgePad = edgePad, endInset = endInset, ) - // Both halves get the same width so the camera slot stays dead centre and the pill reads balanced; the slack - // collects beside the camera while icon and title hug the outer ends, the rule CompactTemplate already uses. - // condensedTitleWidth floors at cellSize, so the icon half is never squeezed. val side = titleWidth + edgePad Row( modifier = Modifier.height(spec.compactHeight), diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt index 8f2a149a68..c9088c5fef 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt @@ -978,40 +978,18 @@ fun IslandNotificationSettingsUI( enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically(), ) { - val condensed = viewModel.isIslandNotifCondensedInfo.value val compactLabel = stringResource(R.string.island_notif_peek_style_compact) val fullLabel = stringResource(R.string.island_notif_peek_style_full) - Column( + SegmentedPicker( + items = listOf(true, false), + selectedItem = viewModel.isIslandNotifCondensedInfo.value, + onItemSelected = { viewModel.setIslandNotifCondensedInfo(it) }, + labelProvider = { if (it) compactLabel else fullLabel }, + title = R.string.island_notif_condensed_info_title, modifier = Modifier .fillMaxWidth() - .background(MaterialTheme.colorScheme.surfaceBright, MaterialTheme.shapes.extraSmall) - .padding(top = 12.dp) .highlight(highlightSetting == "island_notif_condensed_info"), - ) { - Text( - text = stringResource(R.string.island_notif_condensed_info_title), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(horizontal = 16.dp), - ) - Text( - text = stringResource( - if (condensed) R.string.island_notif_condensed_info_desc - else R.string.island_notif_peek_style_full_desc, - ), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 16.dp), - ) - SegmentedPicker( - items = listOf(true, false), - selectedItem = condensed, - onItemSelected = { viewModel.setIslandNotifCondensedInfo(it) }, - labelProvider = { if (it) compactLabel else fullLabel }, - title = R.string.island_notif_condensed_info_title, - modifier = Modifier.fillMaxWidth(), - ) - } + ) } } @@ -1116,7 +1094,6 @@ fun IslandNotificationSettingsUI( } } -// Header sits closer to its own card than to the previous section, so each group reads as one unit. @Composable private fun IslandSettingsSection(titleRes: Int, content: @Composable ColumnScope.() -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d2c82697db..4e23fcc6d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3211,10 +3211,8 @@ Peek new notifications as a line instead of expanding the island Needs Line peek. Tap to turn it on in Behavior Peek style - App icon and title only. Tap to expand for the content and actions Compact Full - Title on the left, message on the right Show notifications Which notifications Heads up From 97abdd1505d36cc36f4589d2f785a25ec4f2b972 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 13 Oct 2026 11:46:30 +0530 Subject: [PATCH 4/4] fix: restore condensed info description used by feature search --- app/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4e23fcc6d7..eb85a1a44e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3211,6 +3211,7 @@ Peek new notifications as a line instead of expanding the island Needs Line peek. Tap to turn it on in Behavior Peek style + App icon and title only. Tap to expand for the content and actions Compact Full Show notifications