fix(widget): Glance child cap, tile gaps, backgrounds and done state
- Week shows as many days as fit; Lists and Week tiles keep their gaps (Glance caps a Row/Column at 10 children and has no margins) - Today and Up next drop the transparent background they can't read on - The Tasks widget's empty state reads as done rather than empty
This commit is contained in:
@@ -22,9 +22,11 @@ import androidx.glance.color.ColorProvider
|
||||
import androidx.glance.currentState
|
||||
import androidx.glance.unit.ColorProvider as GlanceColorProvider
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.height
|
||||
@@ -49,7 +51,6 @@ import de.jeanlucmakiola.floret.glance.WidgetThemeMode
|
||||
import de.jeanlucmakiola.floret.glance.WidgetTile
|
||||
import de.jeanlucmakiola.floret.glance.R as GlanceR
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
@@ -92,7 +93,7 @@ private fun ListsWidgetBody(data: WidgetStatsData?, background: WidgetBackground
|
||||
val tiles = ready?.snapshot?.lists?.let { lists -> listIds?.let { ids -> lists.filter { it.id in ids } } ?: lists }
|
||||
val size = LocalSize.current
|
||||
WidgetSurface(background = background) {
|
||||
Column(modifier = GlanceModifier.fillMaxSize().padding(Padding)) {
|
||||
Column(modifier = GlanceModifier.fillMaxSize().padding(Padding - Gap / 2)) {
|
||||
when {
|
||||
data == null -> Unit
|
||||
ready == null -> WidgetMessage(context.getString(R.string.widget_unavailable))
|
||||
@@ -100,12 +101,15 @@ private fun ListsWidgetBody(data: WidgetStatsData?, background: WidgetBackground
|
||||
else -> {
|
||||
val grid = listGrid(tiles.size, size.width - Padding * 2, size.height - Padding * 2)
|
||||
val rows = tiles.take(grid.columns * grid.rows).chunked(grid.columns)
|
||||
rows.forEachIndexed { index, rowTiles ->
|
||||
if (index > 0) Spacer(GlanceModifier.height(Gap))
|
||||
// Gaps are each tile's padding rather than Spacers, which would
|
||||
// eat into Glance's 10-children-per-Row/Column cap.
|
||||
rows.forEach { rowTiles ->
|
||||
Row(modifier = GlanceModifier.fillMaxWidth().defaultWeight()) {
|
||||
rowTiles.forEachIndexed { i, tile ->
|
||||
if (i > 0) Spacer(GlanceModifier.width(Gap))
|
||||
ListTileCell(tile, grid, GlanceModifier.defaultWeight())
|
||||
rowTiles.forEach { tile ->
|
||||
// Glance has no margin — padding on the tile itself lands inside its fill.
|
||||
Box(modifier = GlanceModifier.defaultWeight().fillMaxHeight().padding(Gap / 2)) {
|
||||
ListTileCell(tile, grid, GlanceModifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,9 +132,9 @@ internal fun listGrid(count: Int, width: Dp, height: Dp): ListGrid {
|
||||
fun span(total: Dp, cells: Int) = (total - Gap * (cells - 1)) / cells
|
||||
fun maxRows(columns: Int): Int {
|
||||
val minHeight = if (span(width, columns) < MinOneLineTileWidth) MinNarrowTileHeight else MinTileHeight
|
||||
return max(1, ((height + Gap) / (minHeight + Gap)).toInt())
|
||||
return ((height + Gap) / (minHeight + Gap)).toInt().coerceIn(1, MaxCells)
|
||||
}
|
||||
val maxColumns = max(1, ((width + Gap) / (MinTileWidth + Gap)).toInt())
|
||||
val maxColumns = ((width + Gap) / (MinTileWidth + Gap)).toInt().coerceIn(1, MaxCells)
|
||||
val best = (1..min(count, maxColumns))
|
||||
.map { columns -> columns to (count + columns - 1) / columns }
|
||||
.filter { (columns, rows) -> rows <= maxRows(columns) }
|
||||
@@ -226,3 +230,6 @@ private val MinNarrowTileHeight = 52.dp
|
||||
private val MinTwoLineNameHeight = 70.dp
|
||||
private val MinOneLineTileWidth = 120.dp
|
||||
private const val TileAspect = 1.5f
|
||||
|
||||
/** Glance's per-Row/Column child cap. */
|
||||
private const val MaxCells = 10
|
||||
|
||||
@@ -60,6 +60,7 @@ import de.jeanlucmakiola.floret.glance.WidgetCheckButton
|
||||
import de.jeanlucmakiola.floret.glance.WidgetGroupedRow
|
||||
import de.jeanlucmakiola.floret.glance.WidgetHeader
|
||||
import de.jeanlucmakiola.floret.glance.WidgetHeaderAction
|
||||
import de.jeanlucmakiola.floret.glance.WidgetDoneState
|
||||
import de.jeanlucmakiola.floret.glance.WidgetMessage
|
||||
import de.jeanlucmakiola.floret.glance.WidgetSurface
|
||||
import de.jeanlucmakiola.floret.glance.WidgetThemeMode
|
||||
@@ -182,7 +183,7 @@ private fun TaskWidgetBody(
|
||||
TaskWidgetData.Unavailable -> WidgetMessage(context.getString(R.string.widget_unavailable))
|
||||
is TaskWidgetData.Ready ->
|
||||
if (data.rows.isEmpty()) {
|
||||
WidgetMessage(context.getString(R.string.widget_empty))
|
||||
WidgetDoneState(context.getString(R.string.widget_empty), context.getString(R.string.widget_empty_hint))
|
||||
} else {
|
||||
val allSections = widgetSections(
|
||||
data.rows,
|
||||
|
||||
@@ -128,7 +128,7 @@ class TaskWidgetConfigActivity : ComponentActivity() {
|
||||
val state = getAppWidgetState(this@TaskWidgetConfigActivity, PreferencesGlanceStateDefinition, glanceId)
|
||||
current = parseWidgetFilter(state[WIDGET_FILTER_KEY])
|
||||
groupByList = state[WIDGET_GROUP_BY_LIST_KEY] ?: false
|
||||
background = parseWidgetBackground(state[WIDGET_BACKGROUND_KEY])
|
||||
background = parseWidgetBackground(state[WIDGET_BACKGROUND_KEY], kind)
|
||||
themeOverride = parseWidgetThemeOverride(state[WIDGET_THEME_KEY])
|
||||
rowColor = parseWidgetRowColor(state[WIDGET_ROW_COLOR_KEY])
|
||||
selectedListIds = parseWidgetListIds(state[WIDGET_LIST_IDS_KEY])
|
||||
@@ -359,7 +359,7 @@ private fun WidgetFilterPicker(
|
||||
if (showBackgroundPicker) {
|
||||
OptionPicker(
|
||||
title = stringResource(R.string.widget_config_background),
|
||||
options = WidgetBackground.entries,
|
||||
options = kind.backgrounds(),
|
||||
selected = background,
|
||||
label = { stringResource(backgroundLabel(it)) },
|
||||
summary = { stringResource(backgroundHint(it)) },
|
||||
|
||||
@@ -68,7 +68,7 @@ class TodayWidget : GlanceAppWidget() {
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val initial = runCatching { context.widgetStats().first() }.getOrDefault(WidgetStatsData.Unavailable)
|
||||
provideContent {
|
||||
val background = parseWidgetBackground(currentState(WIDGET_BACKGROUND_KEY))
|
||||
val background = parseWidgetBackground(currentState(WIDGET_BACKGROUND_KEY), WidgetKind.TODAY)
|
||||
val themeOverride = parseWidgetThemeOverride(currentState(WIDGET_THEME_KEY))
|
||||
val data by remember { context.widgetStats() }.collectAsState(initial = initial)
|
||||
val ready = data as? WidgetStatsData.Ready ?: initial as? WidgetStatsData.Ready
|
||||
|
||||
@@ -83,7 +83,7 @@ class UpNextWidget : GlanceAppWidget() {
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val initial = runCatching { context.widgetStats().first() }.getOrDefault(WidgetStatsData.Unavailable)
|
||||
provideContent {
|
||||
val background = parseWidgetBackground(currentState(WIDGET_BACKGROUND_KEY))
|
||||
val background = parseWidgetBackground(currentState(WIDGET_BACKGROUND_KEY), WidgetKind.UP_NEXT)
|
||||
val themeOverride = parseWidgetThemeOverride(currentState(WIDGET_THEME_KEY))
|
||||
val listId = parseWidgetListIds(currentState(WIDGET_LIST_IDS_KEY))?.singleOrNull()
|
||||
val data by remember { context.widgetStats() }.collectAsState(initial = initial)
|
||||
@@ -184,7 +184,7 @@ private fun UpNextTall(rows: List<WidgetTaskRow>, snapshot: WidgetStatsSnapshot,
|
||||
val accent = if (row.isOverdue) GlanceTheme.colors.error else GlanceTheme.colors.primary
|
||||
val heroSize = (height.value * 0.15f).coerceIn(26f, 38f)
|
||||
val checkSize = (height * 0.2f).coerceIn(44.dp, 56.dp)
|
||||
val laterCount = ((height - MinTallHeight - 40.dp) / LaterRowHeight).toInt().coerceIn(0, rows.size - 1)
|
||||
val laterCount = ((height - MinTallHeight - 40.dp) / LaterRowHeight).toInt().coerceIn(0, minOf(rows.size - 1, MAX_LATER_ROWS))
|
||||
Column(modifier = GlanceModifier.fillMaxSize().padding(12.dp)) {
|
||||
Row(modifier = GlanceModifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
ListDot(row.color)
|
||||
@@ -228,7 +228,7 @@ private fun UpNextTall(rows: List<WidgetTaskRow>, snapshot: WidgetStatsSnapshot,
|
||||
style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = 12.sp, fontWeight = FontWeight.Bold),
|
||||
)
|
||||
Spacer(GlanceModifier.height(4.dp))
|
||||
rows.drop(1).take(laterCount).forEach { later -> LaterRow(later, snapshot.use24Hour) }
|
||||
Column { rows.drop(1).take(laterCount).forEach { later -> LaterRow(later, snapshot.use24Hour) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,6 +404,7 @@ private fun daysUntil(context: Context, due: Instant, now: Instant): String {
|
||||
}
|
||||
|
||||
private const val HOURS_PER_DAY = 24L
|
||||
private const val MAX_LATER_ROWS = 10
|
||||
private val MinTallHeight = 150.dp
|
||||
private val MinRelativeInlineWidth = 200.dp
|
||||
private val LaterRowHeight = 24.dp
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.glance.ColorFilter
|
||||
@@ -15,6 +14,7 @@ import androidx.glance.GlanceTheme
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.LocalContext
|
||||
import androidx.glance.LocalSize
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.appwidget.action.actionStartActivity
|
||||
@@ -27,6 +27,7 @@ import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
@@ -63,7 +64,8 @@ class WeekWidget : GlanceAppWidget() {
|
||||
|
||||
override val stateDefinition = PreferencesGlanceStateDefinition
|
||||
|
||||
override val sizeMode = SizeMode.Responsive(setOf(CompactSize, TallSize))
|
||||
// Exact, so the day count can follow the real width.
|
||||
override val sizeMode = SizeMode.Exact
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val initial = runCatching { context.widgetStats().first() }.getOrDefault(WidgetStatsData.Unavailable)
|
||||
@@ -82,11 +84,6 @@ class WeekWidget : GlanceAppWidget() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val CompactSize = DpSize(250.dp, 56.dp)
|
||||
val TallSize = DpSize(250.dp, 140.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -98,10 +95,16 @@ private fun WeekWidgetBody(data: WidgetStatsData?, background: WidgetBackground)
|
||||
data == null -> Unit
|
||||
ready == null -> WidgetMessage(context.getString(R.string.widget_unavailable), GlanceModifier.padding(8.dp))
|
||||
else -> {
|
||||
Row(modifier = GlanceModifier.fillMaxSize().padding(6.dp)) {
|
||||
ready.snapshot.week.forEachIndexed { index, day ->
|
||||
if (index > 0) Spacer(GlanceModifier.width(4.dp))
|
||||
WeekDayCell(day, GlanceModifier.defaultWeight())
|
||||
// As many days as fit at a comfortable width, up to the week.
|
||||
// Gaps come from each cell's padding, not Spacers: Glance caps a
|
||||
// Row at 10 children.
|
||||
val days = ((LocalSize.current.width - 8.dp) / MinDayWidth).toInt().coerceIn(1, ready.snapshot.week.size)
|
||||
Row(modifier = GlanceModifier.fillMaxSize().padding(4.dp)) {
|
||||
ready.snapshot.week.take(days).forEach { day ->
|
||||
// Glance has no margin — padding on the tile itself lands inside its fill.
|
||||
Box(modifier = GlanceModifier.defaultWeight().fillMaxHeight().padding(horizontal = 2.dp)) {
|
||||
WeekDayCell(day, GlanceModifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,3 +173,5 @@ private fun WeekDayCell(day: WeekDay, modifier: GlanceModifier) {
|
||||
}
|
||||
|
||||
private fun isToday(date: LocalDate): Boolean = date == java.time.LocalDate.now()
|
||||
|
||||
private val MinDayWidth = 64.dp
|
||||
|
||||
@@ -56,6 +56,20 @@ internal fun parseWidgetRowColor(raw: String?): WidgetRowColor =
|
||||
internal fun parseWidgetBackground(raw: String?): WidgetBackground =
|
||||
raw?.let { runCatching { WidgetBackground.valueOf(it) }.getOrNull() } ?: WidgetBackground.SOLID
|
||||
|
||||
/**
|
||||
* [WidgetBackground]s a widget offers. Today and Up next draw their hero (ring
|
||||
* label, time and title) straight on the surface rather than in containers, so
|
||||
* on [WidgetBackground.TRANSPARENT] it would sit unreadable on the wallpaper.
|
||||
*/
|
||||
internal fun WidgetKind.backgrounds(): List<WidgetBackground> = when (this) {
|
||||
WidgetKind.TODAY, WidgetKind.UP_NEXT -> WidgetBackground.entries - WidgetBackground.TRANSPARENT
|
||||
else -> WidgetBackground.entries
|
||||
}
|
||||
|
||||
/** [raw] parsed, falling back to SOLID for a stored value [kind] no longer offers. */
|
||||
internal fun parseWidgetBackground(raw: String?, kind: WidgetKind): WidgetBackground =
|
||||
parseWidgetBackground(raw).takeIf { it in kind.backgrounds() } ?: WidgetBackground.SOLID
|
||||
|
||||
/** `null` means "follow the app's own theme setting" — resolved by the caller against [TaskWidgetData.Ready.themeMode]. */
|
||||
internal fun parseWidgetThemeOverride(raw: String?): WidgetThemeMode? =
|
||||
raw?.let { runCatching { WidgetThemeMode.valueOf(it) }.getOrNull() }
|
||||
|
||||
@@ -755,7 +755,8 @@
|
||||
<!-- Home-screen widget -->
|
||||
<string name="widget_tasks_label">Tasks</string>
|
||||
<string name="widget_tasks_description">Today\'s tasks, or any list, with a tap to tick them off</string>
|
||||
<string name="widget_empty">Nothing to do here</string>
|
||||
<string name="widget_empty">All caught up</string>
|
||||
<string name="widget_empty_hint">Nothing left to do, enjoy it</string>
|
||||
<string name="widget_unavailable">Open Agendula to finish setting up</string>
|
||||
<string name="widget_complete_task">Complete %1$s</string>
|
||||
<string name="widget_config_title">Show in widget</string>
|
||||
|
||||
+1
-1
Submodule floret-kit updated: 8355afc965...cb109c0e82
Reference in New Issue
Block a user