diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/widget/ListsWidget.kt b/app/src/main/java/de/jeanlucmakiola/agendula/widget/ListsWidget.kt index c15475e..5708ae1 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/widget/ListsWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/widget/ListsWidget.kt @@ -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 diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/widget/TaskWidget.kt b/app/src/main/java/de/jeanlucmakiola/agendula/widget/TaskWidget.kt index f14b728..88a8426 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/widget/TaskWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/widget/TaskWidget.kt @@ -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, diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/widget/TaskWidgetConfigActivity.kt b/app/src/main/java/de/jeanlucmakiola/agendula/widget/TaskWidgetConfigActivity.kt index ae2a009..c68cded 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/widget/TaskWidgetConfigActivity.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/widget/TaskWidgetConfigActivity.kt @@ -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)) }, diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/widget/TodayWidget.kt b/app/src/main/java/de/jeanlucmakiola/agendula/widget/TodayWidget.kt index d7d1f97..f5922e1 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/widget/TodayWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/widget/TodayWidget.kt @@ -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 diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/widget/UpNextWidget.kt b/app/src/main/java/de/jeanlucmakiola/agendula/widget/UpNextWidget.kt index 86ab2fa..ad3098a 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/widget/UpNextWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/widget/UpNextWidget.kt @@ -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, 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, 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 diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/widget/WeekWidget.kt b/app/src/main/java/de/jeanlucmakiola/agendula/widget/WeekWidget.kt index 7f69ff1..c1c3c7e 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/widget/WeekWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/widget/WeekWidget.kt @@ -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 diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/widget/WidgetData.kt b/app/src/main/java/de/jeanlucmakiola/agendula/widget/WidgetData.kt index 796470f..4ee3794 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/widget/WidgetData.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/widget/WidgetData.kt @@ -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 = 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() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d1a3f0..9387cb2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -755,7 +755,8 @@ Tasks Today\'s tasks, or any list, with a tap to tick them off - Nothing to do here + All caught up + Nothing left to do, enjoy it Open Agendula to finish setting up Complete %1$s Show in widget diff --git a/floret-kit b/floret-kit index 8355afc..cb109c0 160000 --- a/floret-kit +++ b/floret-kit @@ -1 +1 @@ -Subproject commit 8355afc96591b0a0214401e92fab99c1738e31e6 +Subproject commit cb109c0e82f4513a7fb31f898de1c22128508bb5