Skip to content

Commit a05db5c

Browse files
Allow adding multiple images per activity
1 parent a97bb23 commit a05db5c

File tree

6 files changed

+54
-20
lines changed

6 files changed

+54
-20
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22

3+
- Allow adding multiple images to an activity
4+
35
# 2025.11.1
46

57
- Fix bug where setting the place to none was not possible

app/src/main/java/com/inky/fitnesscalendar/ui/components/ActivityCard.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import com.inky.fitnesscalendar.localization.LocalizationRepository
6262
import com.inky.fitnesscalendar.ui.util.ContextFormat
6363
import com.inky.fitnesscalendar.ui.util.Icons
6464
import com.inky.fitnesscalendar.ui.util.skipToLookaheadSize
65+
import com.inky.fitnesscalendar.util.asNonEmptyOrNull
6566
import com.inky.fitnesscalendar.util.gpx.simplify
6667

6768
@OptIn(ExperimentalFoundationApi::class)
@@ -139,7 +140,7 @@ fun ActivityCard(
139140
}
140141
}
141142

142-
if (images.isNotEmpty()) {
143+
images.asNonEmptyOrNull()?.let { images ->
143144
HorizontalDivider()
144145
ActivityImages(
145146
images = images,

app/src/main/java/com/inky/fitnesscalendar/ui/components/imageComponents.kt

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import androidx.activity.compose.rememberLauncherForActivityResult
77
import androidx.activity.result.PickVisualMediaRequest
88
import androidx.activity.result.contract.ActivityResultContracts
99
import androidx.compose.foundation.clickable
10+
import androidx.compose.foundation.layout.Arrangement
11+
import androidx.compose.foundation.layout.BoxWithConstraints
1012
import androidx.compose.foundation.layout.aspectRatio
11-
import androidx.compose.foundation.layout.fillMaxWidth
13+
import androidx.compose.foundation.layout.width
14+
import androidx.compose.foundation.lazy.LazyRow
15+
import androidx.compose.foundation.lazy.items
1216
import androidx.compose.material3.DropdownMenuItem
1317
import androidx.compose.material3.Icon
1418
import androidx.compose.material3.MaterialTheme
@@ -20,30 +24,38 @@ import androidx.compose.ui.layout.ContentScale
2024
import androidx.compose.ui.platform.LocalContext
2125
import androidx.compose.ui.res.painterResource
2226
import androidx.compose.ui.res.stringResource
27+
import androidx.compose.ui.unit.dp
2328
import coil.compose.AsyncImage
2429
import coil.compose.AsyncImagePainter
2530
import com.inky.fitnesscalendar.R
2631
import com.inky.fitnesscalendar.data.ImageName
32+
import com.inky.fitnesscalendar.util.NonEmptyList
2733
import com.inky.fitnesscalendar.util.copyFileToStorage
2834
import com.inky.fitnesscalendar.util.getOrCreateImagesDir
2935

3036
const val IMAGE_ASPECT_RATIO: Float = 4 / 3f
3137

3238
@Composable
3339
fun ActivityImages(
34-
images: List<ImageName>,
40+
images: NonEmptyList<ImageName>,
3541
modifier: Modifier = Modifier,
3642
onClick: (ImageName) -> Unit = {},
37-
onState: ((AsyncImagePainter.State) -> Unit)? = null,
43+
onState: ((ImageName, AsyncImagePainter.State) -> Unit)? = null,
3844
) {
39-
// TODO: Draw multiple images
40-
val uri = images.map { it.getImageUri() }.first()
41-
ActivityImage(
42-
uri = uri,
43-
modifier = modifier,
44-
onClick = { onClick(images.first()) },
45-
onState = onState
46-
)
45+
val imageScale = if (images.size == 1) 1f else 0.9f
46+
BoxWithConstraints(modifier = modifier) {
47+
val imageWidth = this.maxWidth * imageScale
48+
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
49+
items(images) { image ->
50+
ActivityImage(
51+
uri = image.getImageUri(),
52+
modifier = Modifier.width(imageWidth),
53+
onClick = { onClick(image) },
54+
onState = { state -> onState?.let { it(image, state) } }
55+
)
56+
}
57+
}
58+
}
4759
}
4860

4961
@Composable
@@ -59,7 +71,6 @@ fun ActivityImage(
5971
onState = onState,
6072
contentScale = ContentScale.Crop,
6173
modifier = modifier
62-
.fillMaxWidth()
6374
.aspectRatio(IMAGE_ASPECT_RATIO)
6475
.clip(MaterialTheme.shapes.large)
6576
.clickable { onClick() }

app/src/main/java/com/inky/fitnesscalendar/ui/views/ActivityShareView.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import com.inky.fitnesscalendar.ui.theme.FitnessCalendarTheme
7575
import com.inky.fitnesscalendar.ui.util.Icons
7676
import com.inky.fitnesscalendar.ui.util.SharedContentKey
7777
import com.inky.fitnesscalendar.ui.util.sharedBounds
78+
import com.inky.fitnesscalendar.util.asNonEmptyOrNull
7879
import com.inky.fitnesscalendar.util.getOrCreateSharedMediaCache
7980
import com.inky.fitnesscalendar.util.getOrCreateSharedTracksCache
8081
import com.inky.fitnesscalendar.util.gpx.GpxWriter
@@ -318,7 +319,7 @@ private fun ActivityShareCard(
318319
}
319320

320321
AnimatedVisibility(config.showImage) {
321-
if (images.isNotEmpty()) {
322+
images.asNonEmptyOrNull()?.let { images ->
322323
HorizontalDivider()
323324
ActivityImages(
324325
images = images,

app/src/main/java/com/inky/fitnesscalendar/ui/views/EditActivityView.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import com.inky.fitnesscalendar.ui.components.defaultTopAppBarColors
8181
import com.inky.fitnesscalendar.ui.components.optionGroupDefaultBackground
8282
import com.inky.fitnesscalendar.ui.components.rememberImagePickerLauncher
8383
import com.inky.fitnesscalendar.ui.util.Icons
84+
import com.inky.fitnesscalendar.util.asNonEmptyOrNull
8485
import com.inky.fitnesscalendar.util.someOrNone
8586
import com.inky.fitnesscalendar.util.toDate
8687
import com.inky.fitnesscalendar.util.toLocalDateTime
@@ -297,14 +298,13 @@ fun NewActivity(
297298
.padding(horizontal = 8.dp)
298299
.verticalScroll(scrollState)
299300
) {
300-
val imageNames = editState.images.map { it.imageName }
301-
if (imageNames.isNotEmpty()) {
301+
val imageNames = editState.images.map { it.imageName }.asNonEmptyOrNull()
302+
if (imageNames != null) {
302303
ActivityImages(
303304
images = imageNames,
304-
onState = { state ->
305-
// FIXME: only remove the bad image, instead of all new images
306-
if (state is AsyncImagePainter.State.Error && editState.images != initialState.images) {
307-
onState(editState.copy(images = initialState.images))
305+
onState = { image, state ->
306+
if (state is AsyncImagePainter.State.Error && !initialState.images.any { it.imageName == image }) {
307+
onState(editState.copy(images = editState.images.filter { it.imageName != image }))
308308
}
309309
},
310310
onClick = { imageViewerImage = it },
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.inky.fitnesscalendar.util
2+
3+
interface NonEmptyList<T> : List<T>
4+
5+
@JvmInline
6+
@Suppress("JavaDefaultMethodsNotOverriddenByDelegation")
7+
private value class NonEmptyListImpl<T>(val data: List<T>) : List<T> by data, NonEmptyList<T> {
8+
init {
9+
require(data.isNotEmpty()) { "List must not be empty" }
10+
}
11+
}
12+
13+
14+
fun <T> List<T>.asNonEmptyOrNull(): NonEmptyList<T>? {
15+
if (this.isEmpty()) {
16+
return null
17+
}
18+
return NonEmptyListImpl(this)
19+
}

0 commit comments

Comments
 (0)