UI: lists in grouped surface cards; debug demo seeder
Some checks failed
CI / ci (push) Has been cancelled
Some checks failed
CI / ci (push) Has been cancelled
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user