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