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:
@@ -70,6 +70,7 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
|
|||||||
@@ -12,11 +12,15 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import de.jeanlucmakiola.floret.data.demo.DemoSeeder
|
||||||
import de.jeanlucmakiola.floret.data.prefs.ThemeMode
|
import de.jeanlucmakiola.floret.data.prefs.ThemeMode
|
||||||
import de.jeanlucmakiola.floret.ui.RootScreen
|
import de.jeanlucmakiola.floret.ui.RootScreen
|
||||||
import de.jeanlucmakiola.floret.ui.settings.SettingsViewModel
|
import de.jeanlucmakiola.floret.ui.settings.SettingsViewModel
|
||||||
import de.jeanlucmakiola.floret.ui.theme.FloretTheme
|
import de.jeanlucmakiola.floret.ui.theme.FloretTheme
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single activity. The theme follows [SettingsViewModel]; [RootScreen] is the
|
* Single activity. The theme follows [SettingsViewModel]; [RootScreen] is the
|
||||||
@@ -26,9 +30,17 @@ import de.jeanlucmakiola.floret.ui.theme.FloretTheme
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
@Inject lateinit var demoSeeder: DemoSeeder
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
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 {
|
setContent {
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
val ui by settingsViewModel.state.collectAsStateWithLifecycle()
|
val ui by settingsViewModel.state.collectAsStateWithLifecycle()
|
||||||
@@ -45,6 +57,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_TASK_ID = "de.jeanlucmakiola.floret.extra.TASK_ID"
|
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. */
|
/** Opens the app focused on a task (reminder taps). Routing lands with the UI. */
|
||||||
fun taskIntent(context: Context, taskId: Long): Intent =
|
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.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ListAlt
|
import androidx.compose.material.icons.automirrored.rounded.ListAlt
|
||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.ErrorOutline
|
import androidx.compose.material.icons.rounded.ErrorOutline
|
||||||
import androidx.compose.material.icons.rounded.Today
|
import androidx.compose.material.icons.rounded.Today
|
||||||
import androidx.compose.material.icons.rounded.Upcoming
|
import androidx.compose.material.icons.rounded.Upcoming
|
||||||
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LargeTopAppBar
|
import androidx.compose.material3.LargeTopAppBar
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -111,9 +114,8 @@ private fun ListsContent(
|
|||||||
} else {
|
} else {
|
||||||
item { SectionHeader(stringResource(R.string.lists_header)) }
|
item { SectionHeader(stringResource(R.string.lists_header)) }
|
||||||
state.groups.forEach { group ->
|
state.groups.forEach { group ->
|
||||||
item(key = "acct-${group.accountName}") { AccountHeader(group.accountName) }
|
item(key = "acct-${group.accountName}") {
|
||||||
items(group.lists, key = { it.list.id }) { overview ->
|
AccountGroupCard(group, onOpenFilter)
|
||||||
ListRow(overview) { onOpenFilter(TaskFilter.OfList(overview.list.id)) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
@Composable
|
||||||
private fun ListRow(overview: ListOverview, onClick: () -> Unit) {
|
private fun ListRow(overview: ListOverview, onClick: () -> Unit) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable(onClick = onClick),
|
modifier = Modifier.clickable(onClick = onClick),
|
||||||
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
leadingContent = { ColorDot(overview.list.color) },
|
leadingContent = { ColorDot(overview.list.color) },
|
||||||
headlineContent = { Text(overview.list.name) },
|
headlineContent = { Text(overview.list.name) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
@@ -222,7 +250,7 @@ private fun AccountHeader(text: String) {
|
|||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
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