From 6e1c9bec7939634a90a06f218754c7343a2e5c61 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 17 Jun 2026 22:53:18 +0200 Subject: [PATCH] UI: lists in grouped surface cards; debug demo seeder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Per-account lists now render inside rounded surfaceContainer cards (grouped-list pattern) with inset dividers and transparent rows, instead of bare ListItems on the background — surfaces-first per design feedback. - Add debug-only DemoSeeder (local, non-syncing 'Floret Demo' list) behind BuildConfig.DEBUG + an intent extra, to populate the UI for design review. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/build.gradle.kts | 1 + .../de/jeanlucmakiola/floret/MainActivity.kt | 13 ++++ .../floret/data/demo/DemoSeeder.kt | 60 +++++++++++++++++++ .../floret/ui/lists/ListsScreen.kt | 38 ++++++++++-- 4 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/de/jeanlucmakiola/floret/data/demo/DemoSeeder.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c756ce1..5710471 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -70,6 +70,7 @@ android { buildFeatures { compose = true + buildConfig = true } packaging { diff --git a/app/src/main/java/de/jeanlucmakiola/floret/MainActivity.kt b/app/src/main/java/de/jeanlucmakiola/floret/MainActivity.kt index 9443109..5937d66 100644 --- a/app/src/main/java/de/jeanlucmakiola/floret/MainActivity.kt +++ b/app/src/main/java/de/jeanlucmakiola/floret/MainActivity.kt @@ -12,11 +12,15 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint +import de.jeanlucmakiola.floret.data.demo.DemoSeeder import de.jeanlucmakiola.floret.data.prefs.ThemeMode import de.jeanlucmakiola.floret.ui.RootScreen import de.jeanlucmakiola.floret.ui.settings.SettingsViewModel import de.jeanlucmakiola.floret.ui.theme.FloretTheme +import kotlinx.coroutines.launch +import javax.inject.Inject /** * Single activity. The theme follows [SettingsViewModel]; [RootScreen] is the @@ -26,9 +30,17 @@ import de.jeanlucmakiola.floret.ui.theme.FloretTheme @AndroidEntryPoint class MainActivity : ComponentActivity() { + @Inject lateinit var demoSeeder: DemoSeeder + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + + // Debug-only sample data: `am start ... --ez floret_seed true`. Seeds a + // local (non-syncing) demo list once; no-op without the extra. + if (BuildConfig.DEBUG && intent.getBooleanExtra(EXTRA_SEED, false)) { + lifecycleScope.launch { runCatching { demoSeeder.seed() } } + } setContent { val settingsViewModel: SettingsViewModel = hiltViewModel() val ui by settingsViewModel.state.collectAsStateWithLifecycle() @@ -45,6 +57,7 @@ class MainActivity : ComponentActivity() { companion object { const val EXTRA_TASK_ID = "de.jeanlucmakiola.floret.extra.TASK_ID" + private const val EXTRA_SEED = "floret_seed" /** Opens the app focused on a task (reminder taps). Routing lands with the UI. */ fun taskIntent(context: Context, taskId: Long): Intent = diff --git a/app/src/main/java/de/jeanlucmakiola/floret/data/demo/DemoSeeder.kt b/app/src/main/java/de/jeanlucmakiola/floret/data/demo/DemoSeeder.kt new file mode 100644 index 0000000..0b3c40c --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/floret/data/demo/DemoSeeder.kt @@ -0,0 +1,60 @@ +package de.jeanlucmakiola.floret.data.demo + +import de.jeanlucmakiola.floret.data.di.IoDispatcher +import de.jeanlucmakiola.floret.data.tasks.TasksRepository +import de.jeanlucmakiola.floret.domain.DayWindow +import de.jeanlucmakiola.floret.domain.Priority +import de.jeanlucmakiola.floret.domain.TaskForm +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import java.time.ZoneId +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.time.Clock +import kotlin.time.Instant + +/** + * Debug-only one-shot sample data. Creates a **local, device-only** list + * ("Floret Demo") — so nothing syncs to a CalDAV server — and fills it with + * tasks spread across overdue / today / upcoming / no-date / completed so every + * smart list shows content. Idempotent: skips if the demo list already exists. + * Triggered from [de.jeanlucmakiola.floret.MainActivity] only in debug builds. + */ +@Singleton +class DemoSeeder @Inject constructor( + private val repository: TasksRepository, + @IoDispatcher private val io: CoroutineDispatcher, +) { + suspend fun seed(): Unit = withContext(io) { + val lists = runCatching { repository.taskLists().first() }.getOrElse { return@withContext } + if (lists.any { it.name == DEMO_LIST && it.isLocal }) return@withContext + + val listId = runCatching { repository.createLocalList(DEMO_LIST, DEMO_COLOR) } + .getOrElse { return@withContext } + + val (todayStart, todayEnd) = DayWindow.today(Clock.System.now(), ZoneId.systemDefault()) + val ts = todayStart.toEpochMilliseconds() + val te = todayEnd.toEpochMilliseconds() + val hour = 3_600_000L + val day = 86_400_000L + fun at(millis: Long): Instant = Instant.fromEpochMilliseconds(millis) + + repository.createTask(TaskForm(title = "Water the office plants", listId = listId, due = at(ts - hour), priority = Priority.HIGH)) + repository.createTask(TaskForm(title = "Reply to client email", listId = listId, due = at(te - 2 * hour), priority = Priority.MEDIUM)) + repository.createTask(TaskForm(title = "Write stand-up notes", listId = listId, due = at(te - hour))) + val invoiceId = repository.createTask(TaskForm(title = "Prepare monthly invoice", listId = listId, due = at(te + 2 * day), priority = Priority.MEDIUM)) + repository.createTask(TaskForm(title = "Gather receipts", listId = listId, parentId = invoiceId)) + repository.createTask(TaskForm(title = "Book train tickets", listId = listId, due = at(te + 6 * day), priority = Priority.LOW)) + repository.createTask(TaskForm(title = "Read Compose 1.5 release notes", listId = listId)) + repository.createTask(TaskForm(title = "Sketch the Floret app icon", listId = listId)) + + val done = repository.createTask(TaskForm(title = "Renew domain name", listId = listId, due = at(ts - 2 * day))) + repository.setCompleted(done, completed = true) + } + + private companion object { + const val DEMO_LIST = "Floret Demo" + const val DEMO_COLOR = 0xFF7A5C6B.toInt() + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/floret/ui/lists/ListsScreen.kt b/app/src/main/java/de/jeanlucmakiola/floret/ui/lists/ListsScreen.kt index ced1339..c9e9182 100644 --- a/app/src/main/java/de/jeanlucmakiola/floret/ui/lists/ListsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/floret/ui/lists/ListsScreen.kt @@ -14,21 +14,24 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ListAlt import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.ErrorOutline import androidx.compose.material.icons.rounded.Today import androidx.compose.material.icons.rounded.Upcoming +import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -111,9 +114,8 @@ private fun ListsContent( } else { item { SectionHeader(stringResource(R.string.lists_header)) } state.groups.forEach { group -> - item(key = "acct-${group.accountName}") { AccountHeader(group.accountName) } - items(group.lists, key = { it.list.id }) { overview -> - ListRow(overview) { onOpenFilter(TaskFilter.OfList(overview.list.id)) } + item(key = "acct-${group.accountName}") { + AccountGroupCard(group, onOpenFilter) } } } @@ -178,10 +180,36 @@ private fun SmartCard(count: SmartCount, modifier: Modifier = Modifier, onClick: } } +/** One account's lists as a single rounded surface card (grouped-list pattern). */ +@Composable +private fun AccountGroupCard(group: AccountGroup, onOpenFilter: (TaskFilter) -> Unit) { + Column(Modifier.padding(horizontal = 16.dp)) { + AccountHeader(group.accountName) + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + ) { + group.lists.forEachIndexed { index, overview -> + ListRow(overview) { onOpenFilter(TaskFilter.OfList(overview.list.id)) } + if (index < group.lists.lastIndex) { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(start = 56.dp), + ) + } + } + } + } +} + @Composable private fun ListRow(overview: ListOverview, onClick: () -> Unit) { ListItem( modifier = Modifier.clickable(onClick = onClick), + colors = ListItemDefaults.colors(containerColor = Color.Transparent), leadingContent = { ColorDot(overview.list.color) }, headlineContent = { Text(overview.list.name) }, trailingContent = { @@ -222,7 +250,7 @@ private fun AccountHeader(text: String) { text = text, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 4.dp), + modifier = Modifier.padding(start = 4.dp, top = 12.dp, bottom = 6.dp), ) }