feat(widget): Add 1x1 size support to battery widget

Introduce BatteryLayout enum with size-driven dispatch. Drops minWidth from 80dp to 40dp so the battery widget can be placed at one cell. At 1-cell-wide placements render a compact icon+percent stack; wider sizes keep the current NARROW/WIDE layouts.
This commit is contained in:
darken
2026-04-23 16:11:07 +02:00
committed by Matthias Urhahn
parent 016914ec05
commit 118eaa8d08
8 changed files with 401 additions and 201 deletions
@@ -158,7 +158,7 @@ class AncGlanceWidget : GlanceAppWidget() {
private fun getCellsForSize(size: Int): Int {
var n = 2
while (70 * n - 30 < size) {
while (70 * n - 30 <= size) {
++n
}
return n - 1
@@ -57,12 +57,14 @@ class BatteryGlanceWidget : GlanceAppWidget() {
} catch (e: Exception) {
log(TAG, ERROR) { "provideGlance setup failed: ${e.asLog()}" }
provideContent {
val layout = BatteryLayout.forCells(getCellsForSize(LocalSize.current.width.value.toInt()))
GlanceWidgetContent(
state = WidgetRenderState.Message(
theme = WidgetTheme.DEFAULT,
resolvedBgColor = WidgetRenderStateMapper.resolvedBgColor(context, WidgetTheme.DEFAULT),
resolvedTextColor = WidgetRenderStateMapper.resolvedTextColor(context, WidgetTheme.DEFAULT),
resolvedIconColor = WidgetRenderStateMapper.resolvedIconColor(context, WidgetTheme.DEFAULT),
layout = layout,
primaryText = context.getString(eu.darken.capod.R.string.widget_error_loading_label),
),
context = context,
@@ -77,6 +79,7 @@ class BatteryGlanceWidget : GlanceAppWidget() {
val profiles by ep.deviceProfilesRepo().profiles.collectAsState(initial = emptyList())
val upgradeInfo by ep.upgradeRepo().upgradeInfo.collectAsState(initial = null)
val widthDp = LocalSize.current.width
val layout = BatteryLayout.forCells(getCellsForSize(widthDp.value.toInt()))
val state = try {
val profileId = ep.widgetSettings().getWidgetProfile(appWidgetId)
@@ -93,8 +96,6 @@ class BatteryGlanceWidget : GlanceAppWidget() {
profiles.firstOrNull { it.id == pid }?.label
}
val isWide = getCellsForSize(widthDp.value.toInt()) >= 5
WidgetRenderStateMapper.map(
context = context,
device = device,
@@ -102,7 +103,7 @@ class BatteryGlanceWidget : GlanceAppWidget() {
isPro = isPro,
hasConfiguredProfile = profileId != null,
profileLabel = profileLabel,
isWide = isWide,
layout = layout,
)
} catch (e: Exception) {
log(TAG, ERROR) { "provideGlance failed: ${e.asLog()}" }
@@ -111,6 +112,7 @@ class BatteryGlanceWidget : GlanceAppWidget() {
resolvedBgColor = WidgetRenderStateMapper.resolvedBgColor(context, WidgetTheme.DEFAULT),
resolvedTextColor = WidgetRenderStateMapper.resolvedTextColor(context, WidgetTheme.DEFAULT),
resolvedIconColor = WidgetRenderStateMapper.resolvedIconColor(context, WidgetTheme.DEFAULT),
layout = layout,
primaryText = context.getString(eu.darken.capod.R.string.widget_error_loading_label),
)
}
@@ -137,7 +139,7 @@ class BatteryGlanceWidget : GlanceAppWidget() {
*/
private fun getCellsForSize(size: Int): Int {
var n = 2
while (70 * n - 30 < size) {
while (70 * n - 30 <= size) {
++n
}
return n - 1
@@ -25,6 +25,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.darken.capod.R
@@ -87,86 +88,51 @@ private fun DualPodPreview(
val iconColor = Color(state.resolvedIconColor)
val iconTint = ColorFilter.tint(iconColor)
if (state.isWide) {
// Wide layout: left | case | right in a horizontal row
WidgetContainer(bgColor = bgColor, modifier = modifier) {
Row(
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
// Left pod
PodItemRow(
icon = state.leftIcon,
percent = state.leftPercent,
charging = state.leftCharging,
inEar = state.leftInEar,
when (state.layout) {
BatteryLayout.WIDE -> {
WidgetContainer(bgColor = bgColor, modifier = modifier) {
Row(
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
PodItemRow(state.leftIcon, state.leftPercent, state.leftCharging, state.leftInEar, textColor, iconTint, iconSize = 40, modifier = Modifier.padding(end = 12.dp))
PodItemRow(state.caseIcon, state.casePercent, state.caseCharging, false, textColor, iconTint, iconSize = 40, modifier = Modifier.padding(end = 12.dp))
PodItemRow(state.rightIcon, state.rightPercent, state.rightCharging, state.rightInEar, textColor, iconTint, iconSize = 40)
}
DeviceLabel(
label = state.deviceLabel,
visible = state.theme.showDeviceLabel,
textColor = textColor,
iconTint = iconTint,
iconSize = 40,
modifier = Modifier.padding(end = 12.dp),
)
// Case
PodItemRow(
icon = state.caseIcon,
percent = state.casePercent,
charging = state.caseCharging,
inEar = false,
textColor = textColor,
iconTint = iconTint,
iconSize = 40,
modifier = Modifier.padding(end = 12.dp),
)
// Right pod
PodItemRow(
icon = state.rightIcon,
percent = state.rightPercent,
charging = state.rightCharging,
inEar = state.rightInEar,
textColor = textColor,
iconTint = iconTint,
iconSize = 40,
modifier = Modifier.padding(top = 4.dp, bottom = 8.dp),
)
}
DeviceLabel(
label = state.deviceLabel,
visible = state.theme.showDeviceLabel,
textColor = textColor,
modifier = Modifier.padding(top = 4.dp, bottom = 8.dp),
)
}
} else {
// Compact layout: vertical stack
WidgetContainer(bgColor = bgColor, modifier = modifier) {
PodItemRow(
icon = state.leftIcon,
percent = state.leftPercent,
charging = state.leftCharging,
inEar = state.leftInEar,
textColor = textColor,
iconTint = iconTint,
)
PodItemRow(
icon = state.rightIcon,
percent = state.rightPercent,
charging = state.rightCharging,
inEar = state.rightInEar,
textColor = textColor,
iconTint = iconTint,
)
PodItemRow(
icon = state.caseIcon,
percent = state.casePercent,
charging = state.caseCharging,
inEar = false,
textColor = textColor,
iconTint = iconTint,
)
DeviceLabel(
label = state.deviceLabel,
visible = state.theme.showDeviceLabel,
textColor = textColor,
)
BatteryLayout.NARROW -> {
WidgetContainer(bgColor = bgColor, modifier = modifier) {
PodItemRow(state.leftIcon, state.leftPercent, state.leftCharging, state.leftInEar, textColor, iconTint)
PodItemRow(state.rightIcon, state.rightPercent, state.rightCharging, state.rightInEar, textColor, iconTint)
PodItemRow(state.caseIcon, state.casePercent, state.caseCharging, false, textColor, iconTint)
DeviceLabel(
label = state.deviceLabel,
visible = state.theme.showDeviceLabel,
textColor = textColor,
)
}
}
BatteryLayout.TINY_COLUMN -> {
WidgetContainer(
bgColor = bgColor,
modifier = modifier,
horizontalPadding = 4.dp,
verticalPadding = 0.dp,
) {
TinyPodItem(state.leftIcon, state.leftPercent, textColor, iconTint)
TinyPodItem(state.rightIcon, state.rightPercent, textColor, iconTint)
TinyPodItem(state.caseIcon, state.casePercent, textColor, iconTint)
}
}
}
}
@@ -180,45 +146,60 @@ private fun SinglePodPreview(
val textColor = Color(state.resolvedTextColor)
val iconTint = ColorFilter.tint(Color(state.resolvedIconColor))
WidgetContainer(bgColor = bgColor, modifier = modifier) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(state.batteryIcon),
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = iconTint,
)
Text(
text = formatPercent(state.percent.toBatteryOrNull()),
fontSize = 12.sp,
color = textColor,
modifier = Modifier.padding(horizontal = 8.dp),
)
if (state.charging) {
Image(
painter = painterResource(R.drawable.ic_baseline_power_24),
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = iconTint,
)
}
if (state.worn) {
Image(
painter = painterResource(R.drawable.ic_baseline_hearing_24),
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = iconTint,
when (state.layout) {
BatteryLayout.WIDE, BatteryLayout.NARROW -> {
WidgetContainer(bgColor = bgColor, modifier = modifier) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(state.batteryIcon),
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = iconTint,
)
Text(
text = formatPercent(state.percent.toBatteryOrNull()),
fontSize = 12.sp,
color = textColor,
modifier = Modifier.padding(horizontal = 8.dp),
)
if (state.charging) {
Image(
painter = painterResource(R.drawable.ic_baseline_power_24),
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = iconTint,
)
}
if (state.worn) {
Image(
painter = painterResource(R.drawable.ic_baseline_hearing_24),
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = iconTint,
)
}
}
DeviceLabel(
label = state.deviceLabel,
visible = state.theme.showDeviceLabel,
textColor = textColor,
)
}
}
DeviceLabel(
label = state.deviceLabel,
visible = state.theme.showDeviceLabel,
textColor = textColor,
)
BatteryLayout.TINY_COLUMN -> {
WidgetContainer(
bgColor = bgColor,
modifier = modifier,
horizontalPadding = 4.dp,
verticalPadding = 0.dp,
) {
TinyPodItem(state.batteryIcon, state.percent, textColor, iconTint)
}
}
}
}
@@ -229,17 +210,25 @@ private fun MessagePreview(
) {
val bgColor = Color(state.resolvedBgColor)
val textColor = Color(state.resolvedTextColor)
val isCompact = state.layout == BatteryLayout.TINY_COLUMN
WidgetContainer(bgColor = bgColor, modifier = modifier) {
WidgetContainer(
bgColor = bgColor,
modifier = modifier,
horizontalPadding = if (isCompact) 4.dp else 16.dp,
verticalPadding = if (isCompact) 0.dp else 4.dp,
) {
Text(
text = state.primaryText,
fontSize = 12.sp,
fontSize = if (isCompact) 10.sp else 12.sp,
fontWeight = FontWeight.Bold,
color = textColor,
textAlign = TextAlign.Center,
maxLines = if (isCompact) 2 else Int.MAX_VALUE,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
)
if (state.secondaryText != null) {
if (!isCompact && state.secondaryText != null) {
Text(
text = state.secondaryText,
fontSize = 12.sp,
@@ -260,11 +249,17 @@ private fun LoadingPreview(
) {
val bgColor = Color(state.resolvedBgColor)
val textColor = Color(state.resolvedTextColor)
val isCompact = state.layout == BatteryLayout.TINY_COLUMN
WidgetContainer(bgColor = bgColor, modifier = modifier) {
WidgetContainer(
bgColor = bgColor,
modifier = modifier,
horizontalPadding = if (isCompact) 4.dp else 16.dp,
verticalPadding = if (isCompact) 0.dp else 4.dp,
) {
Text(
text = "",
fontSize = 12.sp,
fontSize = if (isCompact) 10.sp else 12.sp,
fontWeight = FontWeight.Bold,
color = textColor,
textAlign = TextAlign.Center,
@@ -277,13 +272,15 @@ private fun LoadingPreview(
private fun WidgetContainer(
bgColor: Color,
modifier: Modifier = Modifier,
horizontalPadding: Dp = 16.dp,
verticalPadding: Dp = 4.dp,
content: @Composable () -> Unit,
) {
Column(
modifier = modifier
.clip(RoundedCornerShape(16.dp))
.background(bgColor)
.padding(horizontal = 16.dp, vertical = 4.dp),
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
@@ -338,6 +335,34 @@ private fun PodItemRow(
}
}
@Composable
private fun TinyPodItem(
icon: Int,
percent: Float,
textColor: Color,
iconTint: ColorFilter,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(icon),
contentDescription = null,
modifier = Modifier.size(14.dp),
colorFilter = iconTint,
)
Text(
text = formatPercent(percent.toBatteryOrNull()),
fontSize = 12.sp,
color = textColor,
maxLines = 1,
modifier = Modifier.padding(start = 2.dp),
)
}
}
@Composable
private fun DeviceLabel(
label: String?,
@@ -363,12 +388,30 @@ private fun formatPercent(percent: Float?): String {
@Preview2
@Composable
private fun PreviewDualCompact() = PreviewWrapper {
ComposeWidgetPreview(state = WidgetRenderState.previewDualPod())
private fun PreviewDualTiny11() = PreviewWrapper {
ComposeWidgetPreview(
state = WidgetRenderState.previewDualPod(layout = BatteryLayout.TINY_COLUMN),
modifier = Modifier.size(40.dp),
)
}
@Preview2
@Composable
private fun PreviewDualTinyTall() = PreviewWrapper {
ComposeWidgetPreview(
state = WidgetRenderState.previewDualPod(layout = BatteryLayout.TINY_COLUMN),
modifier = Modifier.size(width = 40.dp, height = 110.dp),
)
}
@Preview2
@Composable
private fun PreviewDualNarrow() = PreviewWrapper {
ComposeWidgetPreview(state = WidgetRenderState.previewDualPod(layout = BatteryLayout.NARROW))
}
@Preview2
@Composable
private fun PreviewDualWide() = PreviewWrapper {
ComposeWidgetPreview(state = WidgetRenderState.previewDualPod(isWide = true))
ComposeWidgetPreview(state = WidgetRenderState.previewDualPod(layout = BatteryLayout.WIDE))
}
@@ -3,6 +3,7 @@ package eu.darken.capod.main.ui.widget
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.ColorFilter
@@ -53,31 +54,47 @@ private fun GlanceDualPod(
state: WidgetRenderState.DualPod,
clickModifier: GlanceModifier,
) {
val textStyle = TextStyle(
color = fixedColor(state.resolvedTextColor),
fontSize = 12.sp,
)
val iconTint = ColorFilter.tint(fixedColor(state.resolvedIconColor))
if (state.isWide) {
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
Row(
modifier = GlanceModifier.fillMaxWidth().padding(top = 8.dp, bottom = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
) {
GlancePodItem(state.leftIcon, state.leftPercent, state.leftCharging, state.leftInEar, textStyle, iconTint, iconSize = 40, modifier = GlanceModifier.padding(end = 12.dp))
GlancePodItem(state.caseIcon, state.casePercent, state.caseCharging, false, textStyle, iconTint, iconSize = 40, modifier = GlanceModifier.padding(end = 12.dp))
GlancePodItem(state.rightIcon, state.rightPercent, state.rightCharging, state.rightInEar, textStyle, iconTint, iconSize = 40)
when (state.layout) {
BatteryLayout.WIDE -> {
val textStyle = TextStyle(color = fixedColor(state.resolvedTextColor), fontSize = 12.sp)
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
Row(
modifier = GlanceModifier.fillMaxWidth().padding(top = 8.dp, bottom = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
) {
GlancePodItem(state.leftIcon, state.leftPercent, state.leftCharging, state.leftInEar, textStyle, iconTint, iconSize = 40, modifier = GlanceModifier.padding(end = 12.dp))
GlancePodItem(state.caseIcon, state.casePercent, state.caseCharging, false, textStyle, iconTint, iconSize = 40, modifier = GlanceModifier.padding(end = 12.dp))
GlancePodItem(state.rightIcon, state.rightPercent, state.rightCharging, state.rightInEar, textStyle, iconTint, iconSize = 40)
}
GlanceDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
}
GlanceDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
}
} else {
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
GlancePodItem(state.leftIcon, state.leftPercent, state.leftCharging, state.leftInEar, textStyle, iconTint)
GlancePodItem(state.rightIcon, state.rightPercent, state.rightCharging, state.rightInEar, textStyle, iconTint)
GlancePodItem(state.caseIcon, state.casePercent, state.caseCharging, false, textStyle, iconTint)
GlanceDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
BatteryLayout.NARROW -> {
val textStyle = TextStyle(color = fixedColor(state.resolvedTextColor), fontSize = 12.sp)
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
GlancePodItem(state.leftIcon, state.leftPercent, state.leftCharging, state.leftInEar, textStyle, iconTint)
GlancePodItem(state.rightIcon, state.rightPercent, state.rightCharging, state.rightInEar, textStyle, iconTint)
GlancePodItem(state.caseIcon, state.casePercent, state.caseCharging, false, textStyle, iconTint)
GlanceDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
}
}
BatteryLayout.TINY_COLUMN -> {
val textStyle = TextStyle(color = fixedColor(state.resolvedTextColor), fontSize = 12.sp)
GlanceWidgetRoot(
bgColor = state.resolvedBgColor,
clickModifier = clickModifier,
horizontalPadding = 4.dp,
verticalPadding = 0.dp,
) {
GlanceTinyPodItem(state.leftIcon, state.leftPercent, textStyle, iconTint)
GlanceTinyPodItem(state.rightIcon, state.rightPercent, textStyle, iconTint)
GlanceTinyPodItem(state.caseIcon, state.casePercent, textStyle, iconTint)
}
}
}
}
@@ -87,47 +104,60 @@ private fun GlanceSinglePod(
state: WidgetRenderState.SinglePod,
clickModifier: GlanceModifier,
) {
val textStyle = TextStyle(
color = fixedColor(state.resolvedTextColor),
fontSize = 12.sp,
)
val iconTint = ColorFilter.tint(fixedColor(state.resolvedIconColor))
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
Row(
modifier = GlanceModifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
provider = ImageProvider(state.batteryIcon),
contentDescription = null,
modifier = GlanceModifier.size(20.dp),
colorFilter = iconTint,
)
Text(
text = formatGlancePercent(state.percent.toBatteryOrNull()),
style = textStyle,
modifier = GlanceModifier.padding(horizontal = 8.dp),
)
if (state.charging) {
Image(
provider = ImageProvider(R.drawable.ic_baseline_power_24),
contentDescription = null,
modifier = GlanceModifier.size(20.dp),
colorFilter = iconTint,
)
when (state.layout) {
BatteryLayout.WIDE, BatteryLayout.NARROW -> {
val textStyle = TextStyle(color = fixedColor(state.resolvedTextColor), fontSize = 12.sp)
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
Row(
modifier = GlanceModifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
provider = ImageProvider(state.batteryIcon),
contentDescription = null,
modifier = GlanceModifier.size(20.dp),
colorFilter = iconTint,
)
Text(
text = formatGlancePercent(state.percent.toBatteryOrNull()),
style = textStyle,
modifier = GlanceModifier.padding(horizontal = 8.dp),
)
if (state.charging) {
Image(
provider = ImageProvider(R.drawable.ic_baseline_power_24),
contentDescription = null,
modifier = GlanceModifier.size(20.dp),
colorFilter = iconTint,
)
}
if (state.worn) {
Image(
provider = ImageProvider(R.drawable.ic_baseline_hearing_24),
contentDescription = null,
modifier = GlanceModifier.size(20.dp),
colorFilter = iconTint,
)
}
}
GlanceDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
}
if (state.worn) {
Image(
provider = ImageProvider(R.drawable.ic_baseline_hearing_24),
contentDescription = null,
modifier = GlanceModifier.size(20.dp),
colorFilter = iconTint,
)
}
BatteryLayout.TINY_COLUMN -> {
val textStyle = TextStyle(color = fixedColor(state.resolvedTextColor), fontSize = 12.sp)
GlanceWidgetRoot(
bgColor = state.resolvedBgColor,
clickModifier = clickModifier,
horizontalPadding = 4.dp,
verticalPadding = 0.dp,
) {
GlanceTinyPodItem(state.batteryIcon, state.percent, textStyle, iconTint)
}
}
GlanceDeviceLabel(state.deviceLabel, state.theme.showDeviceLabel, state.resolvedTextColor)
}
}
@@ -136,20 +166,29 @@ private fun GlanceMessage(
state: WidgetRenderState.Message,
clickModifier: GlanceModifier,
) {
val textStyle = TextStyle(
color = fixedColor(state.resolvedTextColor),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
)
val isCompact = state.layout == BatteryLayout.TINY_COLUMN
val fontSize = if (isCompact) 10.sp else 12.sp
val hPad = if (isCompact) 4.dp else 16.dp
val vPad = if (isCompact) 0.dp else 4.dp
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
GlanceWidgetRoot(
bgColor = state.resolvedBgColor,
clickModifier = clickModifier,
horizontalPadding = hPad,
verticalPadding = vPad,
) {
Text(
text = state.primaryText,
style = textStyle,
style = TextStyle(
color = fixedColor(state.resolvedTextColor),
fontSize = fontSize,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
),
modifier = GlanceModifier.fillMaxWidth(),
maxLines = if (isCompact) 2 else Int.MAX_VALUE,
)
if (state.secondaryText != null) {
if (!isCompact && state.secondaryText != null) {
Text(
text = state.secondaryText,
style = TextStyle(
@@ -169,12 +208,18 @@ private fun GlanceLoading(
state: WidgetRenderState.Loading,
clickModifier: GlanceModifier,
) {
GlanceWidgetRoot(state.resolvedBgColor, clickModifier) {
val isCompact = state.layout == BatteryLayout.TINY_COLUMN
GlanceWidgetRoot(
bgColor = state.resolvedBgColor,
clickModifier = clickModifier,
horizontalPadding = if (isCompact) 4.dp else 16.dp,
verticalPadding = if (isCompact) 0.dp else 4.dp,
) {
Text(
text = "",
style = TextStyle(
color = fixedColor(state.resolvedTextColor),
fontSize = 12.sp,
fontSize = if (isCompact) 10.sp else 12.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
),
@@ -187,13 +232,15 @@ private fun GlanceLoading(
private fun GlanceWidgetRoot(
bgColor: Int,
clickModifier: GlanceModifier,
horizontalPadding: Dp = 16.dp,
verticalPadding: Dp = 4.dp,
content: @Composable () -> Unit,
) {
Column(
modifier = clickModifier
.fillMaxSize()
.background(fixedColor(bgColor))
.padding(horizontal = 16.dp, vertical = 4.dp),
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
) {
@@ -246,6 +293,33 @@ private fun GlancePodItem(
}
}
@Composable
private fun GlanceTinyPodItem(
icon: Int,
percent: Float,
textStyle: TextStyle,
iconTint: ColorFilter,
modifier: GlanceModifier = GlanceModifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
provider = ImageProvider(icon),
contentDescription = null,
modifier = GlanceModifier.size(14.dp),
colorFilter = iconTint,
)
Text(
text = formatGlancePercent(percent.toBatteryOrNull()),
style = textStyle,
maxLines = 1,
modifier = GlanceModifier.padding(start = 4.dp),
)
}
}
@Composable
private fun GlanceDeviceLabel(
label: String?,
@@ -4,18 +4,33 @@ import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import eu.darken.capod.R
enum class BatteryLayout {
TINY_COLUMN,
NARROW,
WIDE;
companion object {
fun forCells(widthCells: Int): BatteryLayout = when {
widthCells <= 1 -> TINY_COLUMN
widthCells >= 5 -> WIDE
else -> NARROW
}
}
}
sealed class WidgetRenderState {
abstract val theme: WidgetTheme
@get:ColorInt abstract val resolvedBgColor: Int
@get:ColorInt abstract val resolvedTextColor: Int
@get:ColorInt abstract val resolvedIconColor: Int
abstract val layout: BatteryLayout
data class DualPod(
override val theme: WidgetTheme,
@ColorInt override val resolvedBgColor: Int,
@ColorInt override val resolvedTextColor: Int,
@ColorInt override val resolvedIconColor: Int,
val isWide: Boolean,
override val layout: BatteryLayout,
val deviceLabel: String?,
@DrawableRes val leftIcon: Int,
val leftPercent: Float,
@@ -35,6 +50,7 @@ sealed class WidgetRenderState {
@ColorInt override val resolvedBgColor: Int,
@ColorInt override val resolvedTextColor: Int,
@ColorInt override val resolvedIconColor: Int,
override val layout: BatteryLayout,
val deviceLabel: String?,
@DrawableRes val headsetIcon: Int,
val percent: Float,
@@ -48,6 +64,7 @@ sealed class WidgetRenderState {
@ColorInt override val resolvedBgColor: Int,
@ColorInt override val resolvedTextColor: Int,
@ColorInt override val resolvedIconColor: Int,
override val layout: BatteryLayout,
val primaryText: String,
val secondaryText: String? = null,
) : WidgetRenderState()
@@ -57,6 +74,7 @@ sealed class WidgetRenderState {
@ColorInt override val resolvedBgColor: Int,
@ColorInt override val resolvedTextColor: Int,
@ColorInt override val resolvedIconColor: Int,
override val layout: BatteryLayout,
) : WidgetRenderState()
companion object {
@@ -65,13 +83,13 @@ sealed class WidgetRenderState {
@ColorInt bgColor: Int = 0xFFFFFFFF.toInt(),
@ColorInt textColor: Int = 0xFF1E1E1E.toInt(),
@ColorInt iconColor: Int = 0xFF1E1E1E.toInt(),
isWide: Boolean = false,
layout: BatteryLayout = BatteryLayout.NARROW,
): DualPod = DualPod(
theme = theme,
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
isWide = isWide,
layout = layout,
deviceLabel = "My AirPods Pro",
leftIcon = R.drawable.device_airpods_pro2_left,
leftPercent = 0.85f,
@@ -85,5 +103,25 @@ sealed class WidgetRenderState {
casePercent = 1.0f,
caseCharging = false,
)
fun previewSinglePod(
theme: WidgetTheme = WidgetTheme.DEFAULT,
@ColorInt bgColor: Int = 0xFFFFFFFF.toInt(),
@ColorInt textColor: Int = 0xFF1E1E1E.toInt(),
@ColorInt iconColor: Int = 0xFF1E1E1E.toInt(),
layout: BatteryLayout = BatteryLayout.NARROW,
): SinglePod = SinglePod(
theme = theme,
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
layout = layout,
deviceLabel = "My AirPods Max",
headsetIcon = R.drawable.device_airpods_max,
percent = 0.72f,
batteryIcon = R.drawable.ic_baseline_battery_3_bar_24,
charging = false,
worn = true,
)
}
}
@@ -18,7 +18,7 @@ object WidgetRenderStateMapper {
isPro: Boolean,
hasConfiguredProfile: Boolean,
profileLabel: String?,
isWide: Boolean = false,
layout: BatteryLayout = BatteryLayout.NARROW,
): WidgetRenderState {
val bgColor = resolvedBgColor(context, theme)
val textColor = resolvedTextColor(context, theme)
@@ -30,6 +30,7 @@ object WidgetRenderStateMapper {
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
layout = layout,
primaryText = context.getString(R.string.upgrade_capod_label),
secondaryText = context.getString(R.string.upgrade_capod_description),
)
@@ -39,7 +40,7 @@ object WidgetRenderStateMapper {
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
isWide = isWide,
layout = layout,
deviceLabel = profileLabel ?: device.getLabel(context),
leftIcon = device.leftPodIcon,
leftPercent = device.batteryLeft.toBatteryFloat(),
@@ -59,6 +60,7 @@ object WidgetRenderStateMapper {
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
layout = layout,
deviceLabel = profileLabel ?: device.getLabel(context),
headsetIcon = device.iconRes,
percent = device.batteryHeadset.toBatteryFloat(),
@@ -72,6 +74,7 @@ object WidgetRenderStateMapper {
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
layout = layout,
primaryText = context.getString(R.string.pods_unknown_label),
)
@@ -86,6 +89,7 @@ object WidgetRenderStateMapper {
resolvedBgColor = bgColor,
resolvedTextColor = textColor,
resolvedIconColor = iconColor,
layout = layout,
primaryText = context.getString(messageRes),
)
}
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:minWidth="80dp"
android:minWidth="40dp"
android:minHeight="40dp"
android:targetCellWidth="2"
android:targetCellHeight="1"