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 ddcea132e9..63519c7499 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 @@ -624,6 +624,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" @@ -4178,6 +4179,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 74fb4dda1e..32dfcaf442 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 @@ -819,26 +820,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_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, @@ -976,6 +957,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/model/IslandModels.kt b/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt index bcb4c81286..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 @@ -50,6 +50,8 @@ class LineContent( val end: String, val endSlot: (@Composable () -> Unit)? = null, val showLeftIcon: Boolean = true, + // Icon + title only on line peek + val condensed: Boolean = false, ) interface IslandExpandedScope { @@ -123,6 +125,8 @@ 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 the content currently shown, so a new one pulses even when the item key is unchanged. + 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 82a4ebdc9b..a46da1bfa8 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 @@ -206,7 +206,7 @@ fun IslandRoot( ?.takeIf { outlineAlpha > 0f && fade > 0f } ?.let { it.copy(alpha = it.alpha * outlineAlpha * fade) } 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)) @@ -410,11 +410,9 @@ 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 - } + 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 = if (spec.growDirection > 0) surfaceLeft(width) + width + bubbleGapPx else surfaceLeft(width) - bubbleGapPx - bubbleSizePx val focusedItem = state.focused @@ -433,8 +431,13 @@ 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(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()) } @@ -459,15 +462,18 @@ 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 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) { @@ -635,7 +641,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 3f1d069e0b..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 @@ -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,77 @@ 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 + val titleWidth = condensedTitleWidth( + natural = natural, + lineWidth = spec.lineWidth, + cameraSlotWidth = spec.cameraSlotWidth, + cellSize = spec.cellSize, + edgePad = edgePad, + endInset = endInset, + ) + val side = titleWidth + edgePad + Row( + modifier = Modifier.height(spec.compactHeight), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier.width(side).padding(start = edgePad), + contentAlignment = Alignment.CenterStart, + ) { icon() } + Spacer(Modifier.width(spec.cameraSlotWidth)) + 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 + 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/activities/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt index cf76f2a3b1..3794f9adfc 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 @@ -1334,6 +1336,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 6ec3e63e15..0444787cfd 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 @@ -81,7 +81,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.IslandTimeOptionsBottomSheet @@ -95,18 +94,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( @@ -137,9 +124,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 pickingGesture by remember { mutableStateOf(null) } var showSlideModeSheet by remember { mutableStateOf(false) } @@ -286,7 +270,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" -> IconToggleItem( @@ -769,14 +760,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 b8a48fb4c5..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 @@ -59,11 +59,12 @@ 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.IslandTimeOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandTimerOptionsBottomSheet 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 @@ -75,8 +76,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), @@ -844,3 +852,275 @@ 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 compactLabel = stringResource(R.string.island_notif_peek_style_compact) + val fullLabel = stringResource(R.string.island_notif_peek_style_full) + 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() + .highlight(highlightSetting == "island_notif_condensed_info"), + ) + } + } + + 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, + ) + } +} + +@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 d1eb6411ee..0000000000 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationOptionsBottomSheet.kt +++ /dev/null @@ -1,263 +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"), - ) - } - - 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/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index 530f9a65e1..a7733beea6 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -260,6 +260,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) @@ -914,6 +915,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() @@ -2425,6 +2429,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() @@ -5764,6 +5769,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 33f1906667..eb85a1a44e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3208,7 +3208,22 @@ Skip silent notifications Filter apps Compact heads up - Show new notifications as a line peek + 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 + 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 new file mode 100644 index 0000000000..db7a6d9209 --- /dev/null +++ b/app/src/test/java/com/sameerasw/essentials/island/CondensedLineTest.kt @@ -0,0 +1,56 @@ +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) + + // 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 pillWidth(natural: Dp, endInset: Dp = 0.dp) = side(natural, endInset) * 2 + cameraSlot + + @Test fun shortTitleHugsTheText() { + assertEquals(60.dp, title(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 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 -> + 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) + } +}