diff --git a/conferences-app/src/commonMain/composeResources/values/strings.xml b/conferences-app/src/commonMain/composeResources/values/strings.xml
index 8ac951a8d..c376395d9 100644
--- a/conferences-app/src/commonMain/composeResources/values/strings.xml
+++ b/conferences-app/src/commonMain/composeResources/values/strings.xml
@@ -4,7 +4,8 @@
Upcoming Events
Past Events
- Call for Papers (%1$s days left)
+ CfP Open
+ Call for Papers (%1$s days remaining)
Call for Papers (Closed)
Online Only
diff --git a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventCfp.kt b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventCfp.kt
new file mode 100644
index 000000000..405bef7dd
--- /dev/null
+++ b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventCfp.kt
@@ -0,0 +1,15 @@
+package io.ashdavies.party.events
+
+import kotlinx.datetime.Clock
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.daysUntil
+import kotlinx.datetime.toLocalDateTime
+
+private val Today = Clock.System.now()
+ .toLocalDateTime(TimeZone.currentSystemDefault())
+ .date
+
+internal fun daysUntilCfpEnd(cfpEnd: LocalDate): Int {
+ return Today.daysUntil(cfpEnd)
+}
diff --git a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventDateLabel.kt b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventDateLabel.kt
index 72bbc5697..c64256ea8 100644
--- a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventDateLabel.kt
+++ b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventDateLabel.kt
@@ -1,5 +1,6 @@
package io.ashdavies.party.events
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
@@ -31,6 +32,7 @@ internal fun EventDateLabel(
) {
Column(
modifier = Modifier.padding(MaterialTheme.spacing.small),
+ verticalArrangement = Arrangement.aligned(Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val startMonth = dateStart.format(LocalDate.Format { monthName(MonthNames.ENGLISH_ABBREVIATED) })
diff --git a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsDetailPane.kt b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsDetailPane.kt
index 8a73f4889..5c0b24971 100644
--- a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsDetailPane.kt
+++ b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsDetailPane.kt
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.MyLocation
import androidx.compose.material3.Card
@@ -16,22 +17,32 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
+import androidx.compose.material3.windowsizeclass.WindowSizeClass
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
+import io.ashdavies.party.material.LocalWindowSizeClass
import io.ashdavies.party.material.padding
import io.ashdavies.party.material.spacing
import kotlinx.datetime.LocalDate
import okio.ByteString.Companion.encode
+import org.jetbrains.compose.resources.stringResource
+import playground.conferences_app.generated.resources.Res
+import playground.conferences_app.generated.resources.call_for_papers_closed
+import playground.conferences_app.generated.resources.call_for_papers_days_remaining
@Composable
internal fun EventsDetailPane(
event: Event,
+ onBackClick: () -> Unit,
modifier: Modifier = Modifier,
+ windowSizeClass: WindowSizeClass = LocalWindowSizeClass.current,
) {
Scaffold(
modifier = modifier,
@@ -43,6 +54,16 @@ internal fun EventsDetailPane(
Icon(Icons.Default.Warning, contentDescription = null)
}
},
+ navigationIcon = {
+ if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) {
+ IconButton(onClick = onBackClick) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = null,
+ )
+ }
+ }
+ },
)
},
) { contentPadding ->
@@ -64,13 +85,14 @@ internal fun EventsDetailPane(
}
}
- Card(
- modifier = Modifier
- .padding(MaterialTheme.spacing.large)
- .fillMaxWidth(),
- ) {
- EventsDetailLocation(
- location = event.location,
+ EventsDetailLocation(
+ location = event.location,
+ )
+
+ if (event.cfpEnd != null) {
+ EventsDetailCfp(
+ cfpEnd = event.cfpEnd,
+ cfpSite = event.cfpSite,
)
}
}
@@ -99,18 +121,55 @@ private fun EventsDetailLocation(
location: String,
modifier: Modifier = Modifier,
) {
- Row(
- modifier = modifier,
- verticalAlignment = Alignment.CenterVertically,
+ Card(
+ modifier = modifier
+ .padding(MaterialTheme.spacing.large)
+ .fillMaxWidth(),
) {
- Icon(
- imageVector = Icons.Outlined.MyLocation,
- contentDescription = null,
- modifier = Modifier.padding(16.dp),
- )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.MyLocation,
+ contentDescription = null,
+ modifier = Modifier.padding(16.dp),
+ )
+
+ Column {
+ Text(location)
+ }
+ }
+ }
+}
+
+@Composable
+private fun EventsDetailCfp(
+ cfpSite: String?,
+ cfpEnd: String,
+ modifier: Modifier = Modifier,
+) {
+ val daysUntilCfpEnd = daysUntilCfpEnd(LocalDate.parse(cfpEnd))
+ val uriHandler = LocalUriHandler.current
+ val newModifier = modifier
+ .padding(MaterialTheme.spacing.large)
+ .fillMaxWidth()
- Column {
- Text(location)
+ when {
+ daysUntilCfpEnd > 0 && cfpSite != null -> Card(
+ onClick = { uriHandler.openUri(cfpSite) },
+ modifier = newModifier,
+ ) {
+ Text(
+ text = stringResource(Res.string.call_for_papers_days_remaining, daysUntilCfpEnd),
+ modifier = Modifier.padding(16.dp),
+ )
+ }
+
+ else -> Card(newModifier) {
+ Text(
+ text = stringResource(Res.string.call_for_papers_closed),
+ modifier = Modifier.padding(16.dp),
+ )
}
}
}
diff --git a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsTopBar.kt b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsTopBar.kt
index 5138a6a90..171e06412 100644
--- a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsTopBar.kt
+++ b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsTopBar.kt
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
internal fun EventsTopBar(
title: String,
modifier: Modifier = Modifier,
+ navigationIcon: @Composable () -> Unit = { },
actions: @Composable RowScope.() -> Unit = { },
) {
CenterAlignedTopAppBar(
@@ -25,6 +26,7 @@ internal fun EventsTopBar(
)
},
modifier = modifier,
+ navigationIcon = navigationIcon,
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = MaterialTheme.colorScheme.background,
),
diff --git a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/upcoming/UpcomingEventsPane.kt b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/upcoming/UpcomingEventsPane.kt
index 2c6b31d3b..f6ddabdeb 100644
--- a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/upcoming/UpcomingEventsPane.kt
+++ b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/upcoming/UpcomingEventsPane.kt
@@ -1,12 +1,13 @@
package io.ashdavies.party.upcoming
+import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -20,7 +21,7 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SuggestionChip
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
@@ -28,23 +29,22 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.draw.paint
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.CompositingStrategy
-import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import coil3.compose.AsyncImage
+import coil3.compose.rememberAsyncImagePainter
import io.ashdavies.analytics.OnClick
import io.ashdavies.party.events.Event
import io.ashdavies.party.events.EventDateLabel
import io.ashdavies.party.events.EventsTopBar
+import io.ashdavies.party.events.daysUntilCfpEnd
import io.ashdavies.party.events.paging.errorMessage
import io.ashdavies.party.events.paging.isRefreshing
import io.ashdavies.party.material.padding
@@ -53,24 +53,15 @@ import io.ashdavies.party.paging.items
import io.ashdavies.placeholder.PlaceholderHighlight
import io.ashdavies.placeholder.fade
import io.ashdavies.placeholder.placeholder
-import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
-import kotlinx.datetime.TimeZone
-import kotlinx.datetime.daysUntil
-import kotlinx.datetime.toLocalDateTime
import org.jetbrains.compose.resources.stringResource
import playground.conferences_app.generated.resources.Res
-import playground.conferences_app.generated.resources.call_for_papers_closed
import playground.conferences_app.generated.resources.call_for_papers_open
import playground.conferences_app.generated.resources.online_only
import playground.conferences_app.generated.resources.upcoming_events
private const val EMPTY_STRING = ""
-private val Today = Clock.System.now()
- .toLocalDateTime(TimeZone.currentSystemDefault())
- .date
-
@Composable
@OptIn(ExperimentalMaterial3Api::class)
internal fun UpcomingEventsPane(
@@ -108,7 +99,8 @@ internal fun UpcomingEventsPane(
enabled = event != null,
onClickLabel = event?.name,
onClick = { onClick(event!!) },
- ),
+ )
+ .paint(rememberBackgroundPainter(event?.imageUrl)),
)
}
}
@@ -131,135 +123,101 @@ private fun EventItemContent(
},
),
) {
- Box(Modifier.height(IntrinsicSize.Min)) {
- if (event?.imageUrl != null) {
- EventSectionBackground(
- backgroundImageUrl = event.imageUrl,
- modifier = Modifier.fillMaxSize(),
+ Row(
+ modifier = Modifier
+ .padding(MaterialTheme.spacing.large)
+ .height(IntrinsicSize.Max),
+ horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small.horizontal),
+ ) {
+ Column(Modifier.weight(1f)) {
+ PlaceholderText(
+ text = event?.name,
+ style = MaterialTheme.typography.headlineSmall,
)
- }
- Column(
- modifier = Modifier.padding(MaterialTheme.spacing.large),
- ) {
- Row {
- Column(
- modifier = Modifier.weight(1f),
- ) {
- PlaceholderText(
- text = event?.name,
- style = MaterialTheme.typography.headlineSmall,
- )
-
- PlaceholderText(
- text = event?.location,
- modifier = Modifier.align(Alignment.Start),
- style = MaterialTheme.typography.titleSmall,
- )
- }
+ PlaceholderText(
+ text = event?.location,
+ modifier = Modifier.align(Alignment.Start),
+ style = MaterialTheme.typography.titleSmall,
+ )
+ }
- if (event?.dateStart != null) {
- Spacer(
- modifier = Modifier.width(MaterialTheme.spacing.large.horizontal),
- )
+ if (event?.cfpEnd != null && daysUntilCfpEnd(LocalDate.parse(event.cfpEnd)) > 0) {
+ Column {
+ EventLabel(
+ text = stringResource(Res.string.call_for_papers_open),
+ modifier = Modifier.fillMaxHeight(),
+ )
+ }
+ }
- EventDateLabel(
- dateStart = remember { LocalDate.parse(event.dateStart) },
- dateEnd = remember { LocalDate.parse(event.dateEnd) },
- )
- }
+ if (event?.online != false) {
+ Column {
+ EventLabel(
+ text = stringResource(Res.string.online_only),
+ modifier = Modifier.fillMaxHeight(),
+ )
}
+ }
- EventStatusChips(
- cfpSite = event?.cfpSite,
- cfpEnd = event?.cfpEnd,
- isOnlineOnly = event?.online == true,
- )
+ if (event?.dateStart != null) {
+ Column {
+ EventDateLabel(
+ dateStart = remember { LocalDate.parse(event.dateStart) },
+ dateEnd = remember { LocalDate.parse(event.dateEnd) },
+ modifier = Modifier.fillMaxHeight(),
+ )
+ }
}
}
}
}
@Composable
-private fun EventStatusChips(
- cfpSite: String?,
- cfpEnd: String?,
- isOnlineOnly: Boolean,
+private fun EventLabel(
+ text: String,
modifier: Modifier = Modifier,
) {
- Column(modifier) {
- if (cfpEnd != null && isOnlineOnly) {
- Spacer(Modifier.height(MaterialTheme.spacing.large.vertical))
- }
-
- Row {
- if (cfpEnd != null) {
- val daysUntilCfpEnd = Today.daysUntil(LocalDate.parse(cfpEnd))
- val uriHandler = LocalUriHandler.current
-
- SuggestionChip(
- onClick = { uriHandler.openUri(requireNotNull(cfpSite)) },
- label = {
- Text(
- text = when {
- daysUntilCfpEnd > 0 -> stringResource(
- Res.string.call_for_papers_open,
- daysUntilCfpEnd,
- )
-
- else -> stringResource(Res.string.call_for_papers_closed)
- },
- color = LocalContentColor.current,
- style = MaterialTheme.typography.labelSmall,
- )
- },
- enabled = cfpSite != null && daysUntilCfpEnd > 0,
- shape = MaterialTheme.shapes.small,
- )
- }
-
- if (cfpEnd != null && isOnlineOnly) {
- Spacer(Modifier.width(MaterialTheme.spacing.large.horizontal))
- }
-
- if (isOnlineOnly) {
- SuggestionChip(
- onClick = { },
- label = {
- Text(
- text = stringResource(Res.string.online_only),
- color = LocalContentColor.current,
- style = MaterialTheme.typography.labelSmall,
- )
- },
- shape = MaterialTheme.shapes.small,
- )
- }
+ Surface(
+ modifier = modifier,
+ shape = MaterialTheme.shapes.small,
+ color = Color.Transparent,
+ border = BorderStroke(
+ width = 1.0.dp,
+ color = MaterialTheme.colorScheme.outline,
+ ),
+ ) {
+ Column(
+ modifier = Modifier.fillMaxHeight(),
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(
+ text = text,
+ modifier = Modifier
+ .padding(MaterialTheme.spacing.small)
+ .width(32.dp),
+ color = LocalContentColor.current,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.labelSmall,
+ )
}
}
}
@Composable
-private fun EventSectionBackground(
- backgroundImageUrl: String,
- modifier: Modifier = Modifier,
+private fun rememberBackgroundPainter(
+ backgroundImageUrl: String?,
colorStopStart: Float = 0.25f,
colorStopEnd: Float = 0.5f,
-) {
- val gradientBrush = Brush.horizontalGradient(
+): Painter {
+ @Suppress("UNUSED_VARIABLE")
+ val brush = Brush.horizontalGradient(
colorStopStart to Color.Transparent,
colorStopEnd to Color.Black,
)
- AsyncImage(
+ return rememberAsyncImagePainter(
model = backgroundImageUrl,
- contentDescription = null,
- modifier = modifier
- .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
- .drawWithContent {
- drawContent()
- drawRect(gradientBrush, blendMode = BlendMode.DstIn)
- },
contentScale = ContentScale.Crop,
)
}
diff --git a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/upcoming/UpcomingEventsScreen.kt b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/upcoming/UpcomingEventsScreen.kt
index d0f84f4c0..3d24a3eb9 100644
--- a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/upcoming/UpcomingEventsScreen.kt
+++ b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/upcoming/UpcomingEventsScreen.kt
@@ -59,7 +59,10 @@ internal fun UpcomingEventsScreen(
detailPane = {
AnimatedPane {
navigator.currentDestination?.content?.let {
- EventsDetailPane(it)
+ EventsDetailPane(
+ event = it,
+ onBackClick = navigator::navigateBack,
+ )
}
}
},
diff --git a/conferences-app/src/debug/screenshotTest/reference/io/ashdavies/party/events/EventsDetailTests/EventsDetailPreview_Day_839dc042_0.png b/conferences-app/src/debug/screenshotTest/reference/io/ashdavies/party/events/EventsDetailTests/EventsDetailPreview_Day_839dc042_0.png
index fa0d98b3a..bd0192643 100644
Binary files a/conferences-app/src/debug/screenshotTest/reference/io/ashdavies/party/events/EventsDetailTests/EventsDetailPreview_Day_839dc042_0.png and b/conferences-app/src/debug/screenshotTest/reference/io/ashdavies/party/events/EventsDetailTests/EventsDetailPreview_Day_839dc042_0.png differ
diff --git a/conferences-app/src/debug/screenshotTest/reference/io/ashdavies/party/events/EventsDetailTests/EventsDetailPreview_Night_a6299619_0.png b/conferences-app/src/debug/screenshotTest/reference/io/ashdavies/party/events/EventsDetailTests/EventsDetailPreview_Night_a6299619_0.png
index c983522b0..bd0192643 100644
Binary files a/conferences-app/src/debug/screenshotTest/reference/io/ashdavies/party/events/EventsDetailTests/EventsDetailPreview_Night_a6299619_0.png and b/conferences-app/src/debug/screenshotTest/reference/io/ashdavies/party/events/EventsDetailTests/EventsDetailPreview_Night_a6299619_0.png differ
diff --git a/conferences-app/src/screenshotTest/kotlin/io/ashdavies/party/events/EventsDetailTests.kt b/conferences-app/src/screenshotTest/kotlin/io/ashdavies/party/events/EventsDetailTests.kt
index 4ae5d1dff..71988703a 100644
--- a/conferences-app/src/screenshotTest/kotlin/io/ashdavies/party/events/EventsDetailTests.kt
+++ b/conferences-app/src/screenshotTest/kotlin/io/ashdavies/party/events/EventsDetailTests.kt
@@ -10,7 +10,10 @@ internal class EventsDetailTests {
@PreviewDayNight
private fun EventsDetailPreview() {
MaterialPreviewTheme {
- EventsDetailPane(DroidconBerlin)
+ EventsDetailPane(
+ event = DroidconBerlin,
+ onBackClick = { },
+ )
}
}
}
diff --git a/conferences-app/src/screenshotTest/kotlin/io/ashdavies/party/tooling/MaterialPreviewTheme.kt b/conferences-app/src/screenshotTest/kotlin/io/ashdavies/party/tooling/MaterialPreviewTheme.kt
index a284756f9..9ab9e5357 100644
--- a/conferences-app/src/screenshotTest/kotlin/io/ashdavies/party/tooling/MaterialPreviewTheme.kt
+++ b/conferences-app/src/screenshotTest/kotlin/io/ashdavies/party/tooling/MaterialPreviewTheme.kt
@@ -9,20 +9,27 @@ import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSiz
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import io.ashdavies.party.material.LocalWindowSizeClass
+import org.jetbrains.compose.resources.ExperimentalResourceApi
+import org.jetbrains.compose.resources.PreviewContextConfigurationEffect
@Composable
-@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
+@OptIn(ExperimentalResourceApi::class)
internal fun MaterialPreviewTheme(
size: DpSize = DpSize(1280.dp, 720.dp),
content: @Composable () -> Unit,
) {
- MaterialTheme(if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()) {
- CompositionLocalProvider(
- value = LocalWindowSizeClass provides WindowSizeClass.calculateFromSize(size),
- content = { Surface(content = content) },
- )
+ CompositionLocalProvider(LocalInspectionMode provides true) {
+ PreviewContextConfigurationEffect()
+
+ MaterialTheme(if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()) {
+ @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
+ CompositionLocalProvider(LocalWindowSizeClass provides WindowSizeClass.calculateFromSize(size)) {
+ Surface(content = content)
+ }
+ }
}
}