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 be508fa05..cb545b5ce 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 @@ -546,6 +546,7 @@ class SettingsRepository( const val KEY_ISLAND_BATTERY_HIDE_LOCKED = "island_battery_hide_locked" const val KEY_ISLAND_TIMEOUT_MS = "island_timeout_ms" const val KEY_ISLAND_SHOW_GLOW = "island_show_glow" + const val KEY_ISLAND_MONOCHROME = "island_monochrome" const val KEY_ISLAND_EXPANDED_WIDTH = "island_expanded_width" const val KEY_ISLAND_EXPANDED_ROUNDNESS = "island_expanded_roundness" const val KEY_ISLAND_EXPANDED_PADDING = "island_expanded_padding" @@ -710,9 +711,10 @@ class SettingsRepository( const val KEY_ISLAND_BORDER_OUTLINE_THICKNESS = "island_border_outline_thickness" const val KEY_ISLAND_BORDER_OUTLINE_HIDE_EXPANDED = "island_border_outline_hide_expanded" const val KEY_ISLAND_PULSE_SHADOW_ON_NOTIFICATION = "island_pulse_shadow_on_notification" - const val KEY_ISLAND_PULSE_SHADOW_SIZE = "island_pulse_shadow_size" - const val KEY_ISLAND_PULSE_SHADOW_Y_SHIFT = "island_pulse_shadow_y_shift" - const val KEY_ISLAND_PULSE_SHADOW_SPREAD = "island_pulse_shadow_spread" + const val KEY_ISLAND_PULSE_SHADOW_BLUR = "island_pulse_shadow_blur" + const val KEY_ISLAND_PULSE_SHADOW_SPREAD_DP = "island_pulse_shadow_spread_dp" + const val KEY_ISLAND_PULSE_SHADOW_OFFSET_Y = "island_pulse_shadow_offset_y" + const val KEY_ISLAND_PULSE_SHADOW_OPACITY = "island_pulse_shadow_opacity" const val KEY_ISLAND_PULSE_SHADOW_DURATION_MS = "island_pulse_shadow_duration_ms" const val ISLAND_BORDER_OUTLINE_DEFAULT_COLOR = "#202020" @@ -4078,14 +4080,17 @@ class SettingsRepository( fun isIslandPulseShadowEnabled(): Boolean = getBoolean(KEY_ISLAND_PULSE_SHADOW_ON_NOTIFICATION, false) fun setIslandPulseShadowEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_PULSE_SHADOW_ON_NOTIFICATION, enabled) - fun getIslandPulseShadowSize(): Float = getFloat(KEY_ISLAND_PULSE_SHADOW_SIZE, 0.5f) - fun setIslandPulseShadowSize(value: Float) = putFloat(KEY_ISLAND_PULSE_SHADOW_SIZE, value) + fun getIslandPulseShadowBlur(): Float = getFloat(KEY_ISLAND_PULSE_SHADOW_BLUR, 20f) + fun setIslandPulseShadowBlur(value: Float) = putFloat(KEY_ISLAND_PULSE_SHADOW_BLUR, value) - fun getIslandPulseShadowYShift(): Float = getFloat(KEY_ISLAND_PULSE_SHADOW_Y_SHIFT, 0.35f) - fun setIslandPulseShadowYShift(value: Float) = putFloat(KEY_ISLAND_PULSE_SHADOW_Y_SHIFT, value) + fun getIslandPulseShadowSpread(): Float = getFloat(KEY_ISLAND_PULSE_SHADOW_SPREAD_DP, 6f) + fun setIslandPulseShadowSpread(value: Float) = putFloat(KEY_ISLAND_PULSE_SHADOW_SPREAD_DP, value) - fun getIslandPulseShadowSpread(): Float = getFloat(KEY_ISLAND_PULSE_SHADOW_SPREAD, 2f) - fun setIslandPulseShadowSpread(value: Float) = putFloat(KEY_ISLAND_PULSE_SHADOW_SPREAD, value) + fun getIslandPulseShadowOffsetY(): Float = getFloat(KEY_ISLAND_PULSE_SHADOW_OFFSET_Y, 1f) + fun setIslandPulseShadowOffsetY(value: Float) = putFloat(KEY_ISLAND_PULSE_SHADOW_OFFSET_Y, value) + + fun getIslandPulseShadowOpacity(): Float = getFloat(KEY_ISLAND_PULSE_SHADOW_OPACITY, 0.35f) + fun setIslandPulseShadowOpacity(value: Float) = putFloat(KEY_ISLAND_PULSE_SHADOW_OPACITY, value) fun getIslandPulseShadowDurationMs(): Float = getFloat(KEY_ISLAND_PULSE_SHADOW_DURATION_MS, 1450f) fun setIslandPulseShadowDurationMs(value: Float) = putFloat(KEY_ISLAND_PULSE_SHADOW_DURATION_MS, value) @@ -4093,6 +4098,9 @@ class SettingsRepository( fun isIslandShowGlowEnabled(): Boolean = getBoolean(KEY_ISLAND_SHOW_GLOW, true) fun setIslandShowGlowEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_SHOW_GLOW, enabled) + fun isIslandMonochromeEnabled(): Boolean = getBoolean(KEY_ISLAND_MONOCHROME, false) + fun setIslandMonochromeEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_MONOCHROME, enabled) + fun getIslandExpandedWidth(): Float = getFloat(KEY_ISLAND_EXPANDED_WIDTH, 360f) fun setIslandExpandedWidth(value: Float) = putFloat(KEY_ISLAND_EXPANDED_WIDTH, value) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt index 1ec301ff1..4038351d9 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt @@ -32,5 +32,7 @@ data class ActiveNotificationAlert( val ongoing: Boolean = false, val isConversation: Boolean = false, val hubOnly: Boolean = false, + // The status bar glyph (Notification.smallIcon), used by the monochrome island + val smallIcon: Bitmap? = null, ) 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 4d063ce2a..3d6de90f7 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 @@ -9,6 +9,9 @@ import android.app.KeyguardManager import android.content.Context import android.os.PowerManager import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.runtime.Composable +import android.graphics.Bitmap import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R import com.sameerasw.essentials.data.repository.SettingsRepository @@ -32,6 +35,7 @@ import com.sameerasw.essentials.island.plugins.soften import com.sameerasw.essentials.island.ui.IslandStackBubble import com.sameerasw.essentials.island.ui.IslandStackOverlap import com.sameerasw.essentials.island.ui.components.IslandBitmap +import com.sameerasw.essentials.island.ui.components.NotificationGlyph import com.sameerasw.essentials.island.ui.components.IslandIcon import com.sameerasw.essentials.services.NotificationListener import com.sameerasw.essentials.utils.HapticUtil @@ -46,6 +50,7 @@ class NotificationsPlugin : BaseIslandPlugin() { SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_INLINE, SettingsRepository.KEY_ISLAND_LINE_STAGE_ENABLED, SettingsRepository.KEY_ISLAND_SHOW_GLOW, + SettingsRepository.KEY_ISLAND_MONOCHROME, SettingsRepository.KEY_ISLAND_NOTIF_QUEUE, SettingsRepository.KEY_ISLAND_NOTIF_SKIP_SILENT, SettingsRepository.KEY_ISLAND_NOTIF_FOLLOW_DND, @@ -540,7 +545,7 @@ class NotificationsPlugin : BaseIslandPlugin() { if (!entry.grouped) return stackIconFor(entry.lead, current = false, highlight = highlight) val icon = entry.lead.appIcon ?: entry.lead.icon return StackIcon(entry.key, onSelect = { select(entry.lead.key) }, highlight = highlight) { size -> - IslandBitmap(icon, size, circle = true, fallbackRes = R.drawable.rounded_notifications_unread_24) + AlertIcon(entry.lead, icon, size, circle = true) } } @@ -715,16 +720,16 @@ class NotificationsPlugin : BaseIslandPlugin() { compact = buildList { add( CompactCell("notif.icon") { - IslandBitmap(icon, 22.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) + AlertIcon(alert, icon, 22.dp) }, ) val appIcon = alert.appIcon - if (!minimal() && alert.chatIcon != null && appIcon != null) { + if (!minimal() && !monochrome() && alert.chatIcon != null && appIcon != null) { add(CompactCell("notif.app", soloOnly = true) { IslandBitmap(appIcon, 22.dp, circle = true) }) } }, line = LineContent( - icon = { IslandBitmap(icon, 24.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) }, + icon = { AlertIcon(alert, icon, 24.dp) }, start = sender, end = message, condensed = condensedLine(), @@ -772,24 +777,24 @@ class NotificationsPlugin : BaseIslandPlugin() { compact = buildList { add( CompactCell("notif.icon") { - IslandBitmap(icon, 22.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) + AlertIcon(alert, icon, 22.dp) }, ) val appIcon = alert.appIcon - if (!minimal() && !hideChat && alert.chatIcon != null && appIcon != null) { + if (!minimal() && !monochrome() && !hideChat && alert.chatIcon != null && appIcon != null) { add(CompactCell("notif.app", soloOnly = true) { IslandBitmap(appIcon, 22.dp, circle = true) }) } }, line = if (isOtp(alert)) { LineContent( - icon = { IslandBitmap(alert.appIcon ?: alert.icon, 24.dp, fallbackRes = R.drawable.rounded_lock_24) }, + icon = { AlertIcon(alert, alert.appIcon ?: alert.icon, 24.dp, fallbackRes = R.drawable.rounded_lock_24) }, start = "${appNameFor(context, alert)} ยท ${context.getString(R.string.island_otp_label)}", end = alert.otpCode?.let(::maskOtp).orEmpty(), endSlot = { IslandIcon(R.drawable.rounded_lock_24, size = 18.dp) }, ) } else { LineContent( - icon = { IslandBitmap(icon, 24.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) }, + icon = { AlertIcon(alert, icon, 24.dp) }, start = appNameFor(context, alert), end = "", condensed = condensedLine(), @@ -823,11 +828,11 @@ class NotificationsPlugin : BaseIslandPlugin() { placement = CompactPlacement.Dynamic, compact = listOf( CompactCell("notif.icon") { - IslandBitmap(icon, 22.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) + AlertIcon(alert, icon, 22.dp) }, ), line = LineContent( - icon = { IslandBitmap(icon, 24.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) }, + icon = { AlertIcon(alert, icon, 24.dp) }, start = sender, end = formatOtp(code), endSlot = { IslandIcon(R.drawable.rounded_content_copy_24, size = 18.dp) }, @@ -913,7 +918,25 @@ class NotificationsPlugin : BaseIslandPlugin() { private fun stackIconFor(alert: ActiveNotificationAlert, current: Boolean, highlight: Boolean = false): StackIcon { val icon = if (concealed() && hidesChatPictures()) alert.appIcon ?: alert.icon else alert.chatIcon ?: alert.appIcon ?: alert.icon return StackIcon(alert.key, current = current, onSelect = { select(alert.key) }, highlight = highlight) { size -> - IslandBitmap(icon, size, circle = true, fallbackRes = R.drawable.rounded_notifications_unread_24) + AlertIcon(alert, icon, size, circle = true) + } + } + + private fun monochrome() = settings.isIslandMonochromeEnabled() + + // Monochrome swaps the picture for the app's notification glyph on the island's softened accent + @Composable + private fun AlertIcon( + alert: ActiveNotificationAlert, + bitmap: Bitmap?, + size: Dp, + circle: Boolean = false, + fallbackRes: Int = R.drawable.rounded_notifications_unread_24, + ) { + if (monochrome()) { + NotificationGlyph(alert.smallIcon, alert.appColor?.let { Color(soften(it)) }, size, fallbackRes = fallbackRes) + } else { + IslandBitmap(bitmap, size, circle = circle, fallbackRes = fallbackRes) } } diff --git a/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt b/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt index 9f5fd291d..c13e94359 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt @@ -702,9 +702,10 @@ class IslandCoordinator( outlineThickness = settings.getIslandBorderOutlineThickness().coerceIn(1f, 5f).dp, outlineHiddenWhenExpanded = settings.isIslandBorderOutlineHiddenWhenExpanded(), pulseShadow = settings.isIslandPulseShadowEnabled(), - pulseSize = settings.getIslandPulseShadowSize().coerceIn(0.2f, 1f), - pulseYShift = settings.getIslandPulseShadowYShift().coerceIn(0f, 1f), - pulseSpread = settings.getIslandPulseShadowSpread().coerceIn(1f, 4f), + pulseBlur = settings.getIslandPulseShadowBlur().coerceIn(0f, 48f).dp, + pulseSpread = settings.getIslandPulseShadowSpread().coerceIn(0f, 24f).dp, + pulseOffsetY = settings.getIslandPulseShadowOffsetY().coerceIn(-8f, 16f).dp, + pulseOpacity = settings.getIslandPulseShadowOpacity().coerceIn(0.1f, 1f), pulseDurationMs = settings.getIslandPulseShadowDurationMs().coerceIn(300f, 4000f).toInt(), queueStyle = when (settings.getIslandNotifQueueStyle()) { SettingsRepository.ISLAND_NOTIF_QUEUE_STYLE_OVERLAP -> QueueStyle.Overlap @@ -728,7 +729,6 @@ class IslandCoordinator( controller.maxCells = settings.getIslandMaxItems() * 2 controller.balanceCompact = settings.isIslandBalanceCompactEnabled() controller.secondaryBubbleEnabled = settings.isIslandSecondaryBubbleEnabled() - controller.secondaryBubbleOverlap = settings.getIslandNotifQueueStyle() == SettingsRepository.ISLAND_NOTIF_QUEUE_STYLE_OVERLAP controller.relayout() controller.expandedTimeoutMs = settings.getIslandExpandedTimeoutMs() applyPreviewRing() @@ -822,6 +822,7 @@ class IslandCoordinator( "alarm" to SettingsRepository.KEY_ISLAND_ALARM_LAUNCHER_ONLY, ) val CONFIG_KEYS = setOf( + SettingsRepository.KEY_ISLAND_MONOCHROME, SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_STYLE, SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_INLINE, SettingsRepository.KEY_ISLAND_USE_AUTO_DETECT, @@ -841,9 +842,10 @@ class IslandCoordinator( SettingsRepository.KEY_ISLAND_BORDER_OUTLINE_THICKNESS, SettingsRepository.KEY_ISLAND_BORDER_OUTLINE_HIDE_EXPANDED, SettingsRepository.KEY_ISLAND_PULSE_SHADOW_ON_NOTIFICATION, - SettingsRepository.KEY_ISLAND_PULSE_SHADOW_SIZE, - SettingsRepository.KEY_ISLAND_PULSE_SHADOW_Y_SHIFT, - SettingsRepository.KEY_ISLAND_PULSE_SHADOW_SPREAD, + SettingsRepository.KEY_ISLAND_PULSE_SHADOW_BLUR, + SettingsRepository.KEY_ISLAND_PULSE_SHADOW_SPREAD_DP, + SettingsRepository.KEY_ISLAND_PULSE_SHADOW_OFFSET_Y, + SettingsRepository.KEY_ISLAND_PULSE_SHADOW_OPACITY, SettingsRepository.KEY_ISLAND_PULSE_SHADOW_DURATION_MS, SettingsRepository.KEY_ISLAND_EXPANDED_ROUNDNESS, SettingsRepository.KEY_ISLAND_EXPANDED_PADDING, diff --git a/app/src/main/java/com/sameerasw/essentials/island/ui/IslandLayoutSpec.kt b/app/src/main/java/com/sameerasw/essentials/island/ui/IslandLayoutSpec.kt index 36dc9c7ed..046db4583 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/ui/IslandLayoutSpec.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/ui/IslandLayoutSpec.kt @@ -34,9 +34,10 @@ data class IslandLayoutSpec( val outlineThickness: Dp = 1.dp, val outlineHiddenWhenExpanded: Boolean = false, val pulseShadow: Boolean = false, - val pulseSize: Float = 0.5f, - val pulseYShift: Float = 0.35f, - val pulseSpread: Float = 2f, + val pulseBlur: Dp = 20.dp, + val pulseSpread: Dp = 6.dp, + val pulseOffsetY: Dp = 1.dp, + val pulseOpacity: Float = 0.35f, val pulseDurationMs: Int = 1450, val queueStyle: QueueStyle = QueueStyle.Stack, val queueInline: Boolean = false, 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 a54c5eea0..658c436ac 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 @@ -35,8 +35,6 @@ import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.drawscope.scale import com.sameerasw.essentials.R import androidx.compose.ui.unit.Dp import androidx.compose.ui.res.stringResource @@ -85,6 +83,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.animation.animateColorAsState import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb +import android.graphics.BlurMaskFilter import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.util.VelocityTracker @@ -223,6 +224,10 @@ private fun IslandRootContent( ) val pulseAccent = state.items[state.focusedKey]?.accent + val pulsePaint = remember { android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG) } + val pulseBlur = remember(spec.pulseBlur, density) { + with(density) { spec.pulseBlur.toPx() }.takeIf { it > 0f }?.let { BlurMaskFilter(it * 0.866f, BlurMaskFilter.Blur.NORMAL) } + } val dynamicOutline: Color? = if (spec.outlineDynamic) { val focused = state.items[state.focusedKey] (focused?.outlineAccent ?: focused?.accent) @@ -241,7 +246,7 @@ private fun IslandRootContent( pulse.snapTo(0f) if (!spec.pulseShadow || state.focusedKey == null) return@LaunchedEffect pulse.animateTo(1f, tween(250, easing = LinearEasing)) - pulse.animateTo(0f, tween(1200, easing = LinearEasing)) + pulse.animateTo(0f, tween((spec.pulseDurationMs - 250).coerceAtLeast(100), easing = LinearEasing)) } val minSurfaceWidth = with(density) { spec.cameraDiameter.roundToPx() } val minSurfaceHeight = with(density) { spec.compactHeight.roundToPx() } @@ -872,23 +877,14 @@ private fun IslandRootContent( .drawBehind { val p = pulse.value if (pulseAccent == null || p <= 0f || !visible) return@drawBehind - val w = size.width * spec.pulseSize - val h = size.height * spec.pulseSize - val center = Offset(size.width / 2f, size.height / 2f + h * spec.pulseYShift) - val radius = h / 2f * spec.pulseSpread - scale(scaleX = w / h, scaleY = 1f, pivot = center) { - drawCircle( - brush = Brush.radialGradient( - 0f to pulseAccent.copy(alpha = 0.95f * p), - 0.45f to pulseAccent.copy(alpha = 0.55f * p), - 1f to Color.Transparent, - center = center, - radius = radius, - ), - radius = radius, - center = center, - ) - } + val spread = spec.pulseSpread.toPx() + val dy = spec.pulseOffsetY.toPx() + val r = surfaceCornerPx(size.height, compactHeightPx, expandedCornerPx) + spread + pulsePaint.color = pulseAccent.copy(alpha = spec.pulseOpacity * p).toArgb() + pulsePaint.maskFilter = pulseBlur + drawContext.canvas.nativeCanvas.drawRoundRect( + -spread, -spread + dy, size.width + spread, size.height + spread + dy, r, r, pulsePaint, + ) } .graphicsLayer { alpha = (if (visible) 1f else 0f) * (1f - if (bubbleOnlyLayout) catchUpAnim.value.coerceIn(0f, 1f) else 0f) diff --git a/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt b/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt index 73e380be7..ae76129b5 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt @@ -46,15 +46,52 @@ import com.sameerasw.essentials.island.ui.IslandTextStyles import androidx.compose.ui.unit.sp import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawBehind import androidx.compose.material3.Text import androidx.compose.foundation.layout.Row import com.sameerasw.essentials.island.ui.IslandMotion +import androidx.compose.foundation.background +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.graphics.ColorFilter import kotlinx.coroutines.delay import kotlin.random.Random +// Monochrome stand-in for a notification picture: the app's status bar glyph on its accent color +@Composable +fun NotificationGlyph( + smallIcon: Bitmap?, + color: Color?, + size: Dp, + modifier: Modifier = Modifier, + fallbackRes: Int, +) { + val background = color ?: MaterialTheme.colorScheme.primary + val glyph = lerp(background, if (background.luminance() > 0.179f) Color.Black else Color.White, 0.8f) + Box( + modifier + .sizeIn(maxWidth = size, maxHeight = size) + .aspectRatio(1f, matchHeightConstraintsFirst = true) + .clip(CircleShape) + .background(background), + contentAlignment = Alignment.Center, + ) { + if (smallIcon != null) { + Image( + bitmap = remember(smallIcon) { smallIcon.asImageBitmap() }, + contentDescription = null, + contentScale = ContentScale.Fit, + colorFilter = ColorFilter.tint(glyph), + modifier = Modifier.fillMaxSize(0.6f), + ) + } else { + Icon(painterResource(fallbackRes), null, tint = glyph, modifier = Modifier.fillMaxSize(0.6f)) + } + } +} + @Composable fun IslandBitmap( bitmap: Bitmap?, diff --git a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt index a705aae64..195018f1a 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt @@ -2315,6 +2315,11 @@ class NotificationListener : NotificationListenerService() { bitmap = appIcon } + var smallIconBitmap: Bitmap? = null + try { + notif.smallIcon?.loadDrawable(this)?.let { smallIconBitmap = AppUtil.drawableToBitmap(it) } + } catch (_: Exception) {} + var appColor: Int? = null if (notif.color != 0 && notif.color != android.graphics.Color.TRANSPARENT) { appColor = notif.color @@ -2365,6 +2370,7 @@ class NotificationListener : NotificationListenerService() { appIcon = appIcon, actions = actionList, chatIcon = chatIcon, + smallIcon = smallIconBitmap, ) } 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 3183dadf2..eca6fe760 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 @@ -1257,6 +1257,18 @@ fun IslandNotificationSettingsUI( modifier = Modifier.highlight(highlightSetting == "island_catch_up_timeout"), ) } + + IconToggleItem( + iconRes = R.drawable.rounded_invert_colors_24, + title = stringResource(R.string.island_monochrome_title), + description = stringResource(R.string.island_monochrome_desc), + isChecked = viewModel.isIslandMonochrome.value, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandMonochrome(checked) + }, + modifier = Modifier.highlight(highlightSetting == "island_monochrome"), + ) } if (showOtpPermissions) { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandPulseShadowOptionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandPulseShadowOptionsBottomSheet.kt index 592d165ba..a0f3e4580 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandPulseShadowOptionsBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandPulseShadowOptionsBottomSheet.kt @@ -18,7 +18,7 @@ import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer import com.sameerasw.essentials.ui.core.sheets.EssentialsBottomSheet import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.viewmodels.MainViewModel -import java.util.Locale +import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -45,42 +45,55 @@ fun IslandPulseShadowOptionsBottomSheet( RoundedCardContainer(spacing = 2.dp, cornerRadius = 24.dp) { ConfigSliderItem( - title = stringResource(R.string.island_pulse_shadow_size_title), - value = viewModel.islandPulseShadowSize.floatValue, + title = stringResource(R.string.island_pulse_shadow_blur_title), + value = viewModel.islandPulseShadowBlur.floatValue, onValueChange = { HapticUtil.performUIHaptic(view) - viewModel.setIslandPulseShadowSize(it) + viewModel.setIslandPulseShadowBlur(it) }, - valueRange = 0.2f..1f, - increment = 0.05f, + valueRange = 0f..48f, + increment = 1f, + iconRes = R.drawable.rounded_blur_on_24, + valueFormatter = { "${it.toInt()} dp" }, + ) + + ConfigSliderItem( + title = stringResource(R.string.island_pulse_shadow_spread_title), + value = viewModel.islandPulseShadowSpread.floatValue, + onValueChange = { + HapticUtil.performUIHaptic(view) + viewModel.setIslandPulseShadowSpread(it) + }, + valueRange = 0f..24f, + increment = 1f, iconRes = R.drawable.rounded_magnify_fullscreen_24, - valueFormatter = { "${(it * 100).toInt()}%" }, + valueFormatter = { "${it.toInt()} dp" }, ) ConfigSliderItem( - title = stringResource(R.string.island_pulse_shadow_y_shift_title), - value = viewModel.islandPulseShadowYShift.floatValue, + title = stringResource(R.string.island_pulse_shadow_offset_y_title), + value = viewModel.islandPulseShadowOffsetY.floatValue, onValueChange = { HapticUtil.performUIHaptic(view) - viewModel.setIslandPulseShadowYShift(it) + viewModel.setIslandPulseShadowOffsetY(it) }, - valueRange = 0f..1f, - increment = 0.05f, + valueRange = -8f..16f, + increment = 1f, iconRes = R.drawable.rounded_screenshot_region_24, - valueFormatter = { "${(it * 100).toInt()}%" }, + valueFormatter = { "${it.toInt()} dp" }, ) ConfigSliderItem( - title = stringResource(R.string.island_pulse_shadow_spread_title), - value = viewModel.islandPulseShadowSpread.floatValue, + title = stringResource(R.string.island_pulse_shadow_opacity_title), + value = viewModel.islandPulseShadowOpacity.floatValue, onValueChange = { HapticUtil.performUIHaptic(view) - viewModel.setIslandPulseShadowSpread(it) + viewModel.setIslandPulseShadowOpacity(it) }, - valueRange = 1f..4f, - increment = 0.1f, - iconRes = R.drawable.rounded_blur_on_24, - valueFormatter = { String.format(Locale.US, "%.1fx", it) }, + valueRange = 0.1f..1f, + increment = 0.02f, + iconRes = R.drawable.rounded_invert_colors_24, + valueFormatter = { "${(it * 100).roundToInt()}%" }, ) ConfigSliderItem( 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 e99c0317a..070c2e14e 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -300,15 +300,17 @@ class MainViewModel : ViewModel() { val isIslandOtpEnabled = mutableStateOf(false) val islandOtpTimeoutMs = mutableLongStateOf(60_000L) val isIslandShowGlow = mutableStateOf(true) + val isIslandMonochrome = mutableStateOf(false) val isIslandBorderOutline = mutableStateOf(false) val islandBorderOutlineColor = mutableStateOf(SettingsRepository.ISLAND_BORDER_OUTLINE_DEFAULT_COLOR) val isIslandBorderOutlineDynamic = mutableStateOf(false) val islandBorderOutlineThickness = mutableFloatStateOf(1f) val isIslandBorderOutlineHiddenWhenExpanded = mutableStateOf(false) val isIslandPulseShadow = mutableStateOf(false) - val islandPulseShadowSize = mutableFloatStateOf(0.5f) - val islandPulseShadowYShift = mutableFloatStateOf(0.35f) - val islandPulseShadowSpread = mutableFloatStateOf(2f) + val islandPulseShadowBlur = mutableFloatStateOf(20f) + val islandPulseShadowSpread = mutableFloatStateOf(6f) + val islandPulseShadowOffsetY = mutableFloatStateOf(1f) + val islandPulseShadowOpacity = mutableFloatStateOf(0.35f) val islandPulseShadowDurationMs = mutableFloatStateOf(1450f) val isIslandShowMedia = mutableStateOf(true) val isIslandShowCalendar = mutableStateOf(false) @@ -2579,15 +2581,17 @@ class MainViewModel : ViewModel() { isIslandOtpEnabled.value = settingsRepository.isIslandOtpEnabled() islandOtpTimeoutMs.longValue = settingsRepository.getIslandOtpTimeoutMs() isIslandShowGlow.value = settingsRepository.isIslandShowGlowEnabled() + isIslandMonochrome.value = settingsRepository.isIslandMonochromeEnabled() isIslandBorderOutline.value = settingsRepository.isIslandBorderOutlineEnabled() islandBorderOutlineColor.value = settingsRepository.getIslandBorderOutlineColor() isIslandBorderOutlineDynamic.value = settingsRepository.isIslandBorderOutlineDynamicEnabled() islandBorderOutlineThickness.floatValue = settingsRepository.getIslandBorderOutlineThickness() isIslandBorderOutlineHiddenWhenExpanded.value = settingsRepository.isIslandBorderOutlineHiddenWhenExpanded() isIslandPulseShadow.value = settingsRepository.isIslandPulseShadowEnabled() - islandPulseShadowSize.floatValue = settingsRepository.getIslandPulseShadowSize() - islandPulseShadowYShift.floatValue = settingsRepository.getIslandPulseShadowYShift() + islandPulseShadowBlur.floatValue = settingsRepository.getIslandPulseShadowBlur() islandPulseShadowSpread.floatValue = settingsRepository.getIslandPulseShadowSpread() + islandPulseShadowOffsetY.floatValue = settingsRepository.getIslandPulseShadowOffsetY() + islandPulseShadowOpacity.floatValue = settingsRepository.getIslandPulseShadowOpacity() islandPulseShadowDurationMs.floatValue = settingsRepository.getIslandPulseShadowDurationMs() isIslandShowMedia.value = settingsRepository.isIslandShowMediaEnabled() isIslandShowCalendar.value = settingsRepository.isIslandShowCalendarEnabled() @@ -6165,6 +6169,11 @@ class MainViewModel : ViewModel() { settingsRepository.setIslandShowGlowEnabled(enabled) } + fun setIslandMonochrome(enabled: Boolean) { + isIslandMonochrome.value = enabled + settingsRepository.setIslandMonochromeEnabled(enabled) + } + fun setIslandBorderOutline(enabled: Boolean) { isIslandBorderOutline.value = enabled settingsRepository.setIslandBorderOutlineEnabled(enabled) @@ -6195,14 +6204,19 @@ class MainViewModel : ViewModel() { settingsRepository.setIslandPulseShadowEnabled(enabled) } - fun setIslandPulseShadowSize(value: Float) { - islandPulseShadowSize.floatValue = value - settingsRepository.setIslandPulseShadowSize(value) + fun setIslandPulseShadowBlur(value: Float) { + islandPulseShadowBlur.floatValue = value + settingsRepository.setIslandPulseShadowBlur(value) + } + + fun setIslandPulseShadowOffsetY(value: Float) { + islandPulseShadowOffsetY.floatValue = value + settingsRepository.setIslandPulseShadowOffsetY(value) } - fun setIslandPulseShadowYShift(value: Float) { - islandPulseShadowYShift.floatValue = value - settingsRepository.setIslandPulseShadowYShift(value) + fun setIslandPulseShadowOpacity(value: Float) { + islandPulseShadowOpacity.floatValue = value + settingsRepository.setIslandPulseShadowOpacity(value) } fun setIslandPulseShadowSpread(value: Float) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0f50484bf..f02ab0971 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3224,11 +3224,14 @@ Only outline the compact and peek island Show pulse shadow on notification Briefly glows behind the island in the app color - Glow size - Vertical shift - Glow spread + Blur + Vertical offset + Spread + Opacity Pulse duration App based color glow + App icon bubble + Show the app\'s notification icon on its app color instead of the picture or content Expanded width Camera gap Width when long pressed to expand