@imgly/editor-react-native 1.81.0-rc.0 → 1.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/android/build.gradle +4 -1
  2. package/package.json +1 -1
  3. package/src/NativeIMGLYEditor.ts +1 -1
  4. package/android/src/main/assets/ly.img.memories.style/content.json +0 -50
  5. package/android/src/main/assets/ly.img.memories.style/thumbnails/bubblegum.png +0 -0
  6. package/android/src/main/assets/ly.img.memories.style/thumbnails/hologram.png +0 -0
  7. package/android/src/main/assets/ly.img.memories.style/thumbnails/noir.png +0 -0
  8. package/android/src/main/assets/ly.img.memories.style/videos/bubblegum.mp4 +0 -0
  9. package/android/src/main/assets/ly.img.memories.style/videos/hologram.mp4 +0 -0
  10. package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesApp.kt +0 -77
  11. package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesConfiguration.kt +0 -38
  12. package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesEditor.kt +0 -79
  13. package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesViewModel.kt +0 -469
  14. package/android/src/main/kotlin/ly/img/editor/configuration/memories/callback/OnCreate.kt +0 -69
  15. package/android/src/main/kotlin/ly/img/editor/configuration/memories/callback/OnExport.kt +0 -46
  16. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/BottomPanel.kt +0 -151
  17. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/Dock.kt +0 -145
  18. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/ExportOverlay.kt +0 -229
  19. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/NavigationBar.kt +0 -80
  20. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/Overlay.kt +0 -138
  21. package/android/src/main/kotlin/ly/img/editor/configuration/memories/extension/EditorScopeExt.kt +0 -111
  22. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/AddPhotoAlternate.kt +0 -79
  23. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/ArrowForward.kt +0 -45
  24. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Audiotrack.kt +0 -43
  25. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Check.kt +0 -39
  26. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/CheckCircle.kt +0 -44
  27. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Close.kt +0 -43
  28. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Delete.kt +0 -54
  29. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/DeleteOutline.kt +0 -53
  30. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Fullscreen.kt +0 -74
  31. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Image.kt +0 -51
  32. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Info.kt +0 -48
  33. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Pause.kt +0 -46
  34. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/PlayArrow.kt +0 -38
  35. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/RepeatOff.kt +0 -80
  36. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/RepeatOn.kt +0 -59
  37. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/SkipPrevious.kt +0 -46
  38. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Sort.kt +0 -55
  39. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Style.kt +0 -59
  40. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Title.kt +0 -43
  41. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/TouchTriple.kt +0 -134
  42. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/VolumeDown.kt +0 -48
  43. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/VolumeUp.kt +0 -60
  44. package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/ExportStatus.kt +0 -20
  45. package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/ImageItem.kt +0 -8
  46. package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/TimelineImage.kt +0 -18
  47. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Events.kt +0 -59
  48. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Playback.kt +0 -29
  49. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/SceneSetup.kt +0 -79
  50. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Timeline.kt +0 -27
  51. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Title.kt +0 -162
  52. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/TrackEditor.kt +0 -740
  53. package/android/src/main/kotlin/ly/img/editor/configuration/memories/screen/ImageSelectionScreen.kt +0 -387
  54. package/android/src/main/kotlin/ly/img/editor/configuration/memories/screen/LoadingScreen.kt +0 -258
  55. package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/FilterStylesSheet.kt +0 -167
  56. package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/FilterStylesSheetType.kt +0 -21
  57. package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/VolumeSheet.kt +0 -104
  58. package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/StyleApplier.kt +0 -220
  59. package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/StyleAssetSource.kt +0 -43
  60. package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/VideoStyle.kt +0 -118
  61. package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Animations.kt +0 -58
  62. package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Constants.kt +0 -20
  63. package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/ImageDate.kt +0 -81
  64. package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Utils.kt +0 -27
  65. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/CenteredAddImageButton.kt +0 -82
  66. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/DraggableImageItem.kt +0 -145
  67. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/FullscreenVideoControls.kt +0 -136
  68. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageGrid.kt +0 -122
  69. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageGridOverlay.kt +0 -149
  70. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageTimelineStrip.kt +0 -323
  71. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageToolbar.kt +0 -401
  72. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ReorderableGridState.kt +0 -183
  73. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/VideoControlPanel.kt +0 -180
  74. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/VideoThumbnail.kt +0 -110
@@ -1,149 +0,0 @@
1
- package ly.img.editor.configuration.memories.widget
2
-
3
- import android.net.Uri
4
- import androidx.activity.compose.rememberLauncherForActivityResult
5
- import androidx.activity.result.PickVisualMediaRequest
6
- import androidx.activity.result.contract.ActivityResultContracts
7
- import androidx.compose.foundation.background
8
- import androidx.compose.foundation.layout.Box
9
- import androidx.compose.foundation.layout.PaddingValues
10
- import androidx.compose.foundation.layout.fillMaxSize
11
- import androidx.compose.foundation.layout.fillMaxWidth
12
- import androidx.compose.foundation.layout.height
13
- import androidx.compose.foundation.layout.navigationBarsPadding
14
- import androidx.compose.foundation.layout.padding
15
- import androidx.compose.foundation.layout.size
16
- import androidx.compose.foundation.layout.statusBarsPadding
17
- import androidx.compose.foundation.shape.CircleShape
18
- import androidx.compose.material3.Icon
19
- import androidx.compose.material3.IconButton
20
- import androidx.compose.material3.MaterialTheme
21
- import androidx.compose.runtime.Composable
22
- import androidx.compose.runtime.rememberCoroutineScope
23
- import androidx.compose.ui.Alignment
24
- import androidx.compose.ui.Modifier
25
- import androidx.compose.ui.graphics.Brush
26
- import androidx.compose.ui.graphics.Color
27
- import androidx.compose.ui.platform.LocalContext
28
- import androidx.compose.ui.unit.dp
29
- import kotlinx.coroutines.launch
30
- import ly.img.editor.configuration.memories.iconPack.Close
31
- import ly.img.editor.configuration.memories.model.ImageItem
32
- import ly.img.editor.configuration.memories.util.buildImageItems
33
- import ly.img.editor.core.iconpack.IconPack
34
-
35
- /**
36
- * Full-screen overlay for editing the slideshow's images while inside the editor.
37
- *
38
- * All edits (add / sort / reorder / delete) are staged in [images] and are only written to the
39
- * tracks when the user taps the confirm (✓) button. Tapping the close (X) button discards them.
40
- */
41
- @Composable
42
- fun ImageGridOverlay(
43
- images: List<ImageItem>,
44
- onImagesChanged: (List<ImageItem>) -> Unit,
45
- onAddImages: (List<ImageItem>) -> Unit,
46
- onClose: () -> Unit,
47
- onApply: () -> Unit,
48
- selectedForDeletion: Set<Int> = emptySet(),
49
- onToggleSelection: (Int) -> Unit = {},
50
- isSortEnabled: Boolean = false,
51
- isSortAscending: Boolean = true,
52
- onToggleSort: () -> Unit = {},
53
- onDeleteSelected: () -> Unit = {},
54
- onClearAll: () -> Unit = {},
55
- multiSelectMode: Boolean = false,
56
- onToggleMultiSelect: () -> Unit = {},
57
- ) {
58
- val context = LocalContext.current
59
- val scope = rememberCoroutineScope()
60
-
61
- // Media picker (images + videos) — added items get unique ids.
62
- val photoPickerLauncher = rememberLauncherForActivityResult(
63
- contract = ActivityResultContracts.PickMultipleVisualMedia(),
64
- ) { uris: List<Uri> ->
65
- if (uris.isNotEmpty()) {
66
- val baseId = images.maxOfOrNull { it.id } ?: 0
67
- scope.launch {
68
- onAddImages(buildImageItems(context.contentResolver, uris, baseId))
69
- }
70
- }
71
- }
72
-
73
- Box(
74
- modifier = Modifier
75
- .fillMaxSize()
76
- .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.98f))
77
- .statusBarsPadding(),
78
- ) {
79
- Box(modifier = Modifier.fillMaxSize()) {
80
- ImageGrid(
81
- images = images,
82
- onImagesChanged = onImagesChanged, // engine == null → reorders the draft only
83
- contentPadding = PaddingValues(
84
- top = 72.dp,
85
- bottom = 90.dp,
86
- start = 16.dp,
87
- end = 16.dp,
88
- ),
89
- selectedForDeletion = if (multiSelectMode) selectedForDeletion else emptySet(),
90
- onToggleSelection = if (multiSelectMode) onToggleSelection else null,
91
- )
92
- }
93
-
94
- Box(
95
- modifier = Modifier
96
- .fillMaxWidth()
97
- .height(72.dp)
98
- .background(
99
- brush = Brush.verticalGradient(
100
- colors = listOf(
101
- MaterialTheme.colorScheme.background,
102
- Color.Transparent,
103
- ),
104
- ),
105
- ),
106
- )
107
-
108
- // Bottom toolbar — confirm (✓) applies the staged changes to the tracks.
109
- ImageToolbar(
110
- modifier = Modifier
111
- .align(Alignment.BottomCenter)
112
- .padding(horizontal = 16.dp)
113
- .navigationBarsPadding(),
114
- onAddImage = { photoPickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo)) },
115
- onToggleSort = onToggleSort,
116
- onDeleteAll = onClearAll,
117
- onDeleteSelected = onDeleteSelected,
118
- onProceed = onApply,
119
- isSortEnabled = isSortEnabled,
120
- isSortAscending = isSortAscending,
121
- hasImages = images.isNotEmpty(),
122
- showDeleteButton = true,
123
- selectedForDeletionCount = selectedForDeletion.size,
124
- totalCount = images.size,
125
- confirmMode = true,
126
- multiSelectMode = multiSelectMode,
127
- onToggleMultiSelect = onToggleMultiSelect,
128
- )
129
-
130
- // Close button (X) at the top right — discards the staged changes.
131
- IconButton(
132
- onClick = onClose,
133
- modifier = Modifier
134
- .align(Alignment.TopEnd)
135
- .size(72.dp)
136
- .padding(20.dp)
137
- .background(
138
- MaterialTheme.colorScheme.surface.copy(alpha = 0.8f),
139
- CircleShape,
140
- ),
141
- ) {
142
- Icon(
143
- imageVector = IconPack.Close,
144
- contentDescription = "Cancel",
145
- tint = MaterialTheme.colorScheme.onSurface,
146
- )
147
- }
148
- }
149
- }
@@ -1,323 +0,0 @@
1
- @file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
2
-
3
- package ly.img.editor.configuration.memories.widget
4
-
5
- import androidx.compose.animation.core.animateDpAsState
6
- import androidx.compose.foundation.background
7
- import androidx.compose.foundation.clickable
8
- import androidx.compose.foundation.interaction.collectIsDraggedAsState
9
- import androidx.compose.foundation.layout.Box
10
- import androidx.compose.foundation.layout.BoxWithConstraints
11
- import androidx.compose.foundation.layout.PaddingValues
12
- import androidx.compose.foundation.layout.fillMaxHeight
13
- import androidx.compose.foundation.layout.fillMaxSize
14
- import androidx.compose.foundation.layout.fillMaxWidth
15
- import androidx.compose.foundation.layout.height
16
- import androidx.compose.foundation.layout.padding
17
- import androidx.compose.foundation.layout.size
18
- import androidx.compose.foundation.pager.HorizontalPager
19
- import androidx.compose.foundation.pager.PageSize
20
- import androidx.compose.foundation.pager.rememberPagerState
21
- import androidx.compose.foundation.shape.CircleShape
22
- import androidx.compose.foundation.shape.RoundedCornerShape
23
- import androidx.compose.foundation.systemGestureExclusion
24
- import androidx.compose.material3.Icon
25
- import androidx.compose.material3.MaterialTheme
26
- import androidx.compose.runtime.Composable
27
- import androidx.compose.runtime.LaunchedEffect
28
- import androidx.compose.runtime.getValue
29
- import androidx.compose.runtime.mutableStateOf
30
- import androidx.compose.runtime.remember
31
- import androidx.compose.runtime.rememberCoroutineScope
32
- import androidx.compose.runtime.rememberUpdatedState
33
- import androidx.compose.runtime.setValue
34
- import androidx.compose.runtime.snapshotFlow
35
- import androidx.compose.ui.Alignment
36
- import androidx.compose.ui.Modifier
37
- import androidx.compose.ui.draw.alpha
38
- import androidx.compose.ui.draw.clip
39
- import androidx.compose.ui.graphics.Color
40
- import androidx.compose.ui.hapticfeedback.HapticFeedbackType
41
- import androidx.compose.ui.layout.ContentScale
42
- import androidx.compose.ui.platform.LocalHapticFeedback
43
- import androidx.compose.ui.unit.dp
44
- import coil.compose.AsyncImage
45
- import kotlinx.coroutines.launch
46
- import ly.img.editor.configuration.memories.iconPack.PlayArrow
47
- import ly.img.editor.configuration.memories.iconPack.Title
48
- import ly.img.editor.configuration.memories.model.TimelineImage
49
- import ly.img.editor.core.iconpack.IconPack
50
- import ly.img.engine.Engine
51
- import kotlin.math.absoluteValue
52
-
53
- private val ITEM_HEIGHT = 60.dp
54
- private val ITEM_WIDTH = ITEM_HEIGHT * 9f / 16f // 9:16 portrait, not square
55
-
56
- /**
57
- * A single thumbnail in the timeline pager. [url] is null for the title item (a "T" icon).
58
- * [seekTarget] is where tapping/scrolling seeks to. [currentThreshold] is the playback time at
59
- * which this item becomes the on-screen ("current") one and its played-overlay starts filling.
60
- */
61
- data class StripItem(
62
- val url: String?,
63
- val startTime: Double,
64
- val endTime: Double,
65
- val seekTarget: Double,
66
- val currentThreshold: Double,
67
- val isVideo: Boolean = false,
68
- )
69
-
70
- /** Build the pager items: an optional leading title item, then the timeline images. */
71
- fun buildStripItems(
72
- images: List<TimelineImage>,
73
- titleClearTime: Double?,
74
- ): List<StripItem> = buildList {
75
- if (titleClearTime != null) {
76
- val firstImageStart = images.firstOrNull()?.startTime ?: (titleClearTime + 1.0)
77
- add(
78
- StripItem(
79
- url = null,
80
- startTime = 0.0,
81
- endTime = firstImageStart,
82
- seekTarget = titleClearTime,
83
- currentThreshold = 0.0,
84
- ),
85
- )
86
- }
87
- images.forEachIndexed { index, it ->
88
- if (it.isVideo) {
89
- // Videos have no enter animation: they become current at their start, and tapping
90
- // seeks to the start of the clip (not into a non-existent fade).
91
- add(
92
- StripItem(
93
- url = it.url,
94
- startTime = it.startTime,
95
- endTime = it.endTime,
96
- seekTarget = it.startTime,
97
- currentThreshold = it.startTime,
98
- isVideo = true,
99
- ),
100
- )
101
- } else {
102
- // A photo becomes "current" the moment it overtakes the previous clip. If it crossfades
103
- // in (photo→photo), that's the crossfade midpoint — halfway through the region where the
104
- // two clips are both on screen ([startTime, prevEnd]). If it hard-cuts in — the first
105
- // clip, or a photo right after a video, where there is no overlap — it is current at its
106
- // own start; using the crossfade midpoint there would lag the strip by OVERLAP/2 while
107
- // the photo is already full-screen. Tapping still seeks to the clearest (midpoint) frame.
108
- val prev = images.getOrNull(index - 1)
109
- val crossfadesIn = prev != null && prev.endTime > it.startTime
110
- val currentThreshold = if (crossfadesIn) (it.startTime + prev!!.endTime) / 2.0 else it.startTime
111
- add(
112
- StripItem(
113
- url = it.url,
114
- startTime = it.startTime,
115
- endTime = it.endTime,
116
- seekTarget = it.fullyVisibleTime,
117
- currentThreshold = currentThreshold,
118
- ),
119
- )
120
- }
121
- }
122
- }
123
-
124
- /**
125
- * Horizontal pager of every timeline image (plus an optional title item).
126
- *
127
- * - All items are the same size; the current item snaps to the center (centering content padding).
128
- * - Scrolling to / tapping an item seeks to it; playback scrolls the pager to follow.
129
- * - Each thumbnail's overlay grows left→right from 0% right after its enter animation finishes to
130
- * 100% when its exit animation finishes (already-played items stay fully covered).
131
- *
132
- * [buildItems] reads the current items from the engine; it is re-invoked on first composition and on
133
- * every [Engine.editor] history change (apply, undo, redo) so the strip stays in sync — in
134
- * particular it refreshes when the user taps undo.
135
- */
136
- @Composable
137
- fun ImageTimelineStrip(
138
- engine: Engine,
139
- buildItems: () -> List<StripItem>,
140
- currentTime: Double,
141
- onSeekToTime: (Double) -> Unit,
142
- modifier: Modifier = Modifier,
143
- ) {
144
- val latestBuild by rememberUpdatedState(buildItems)
145
- var items by remember { mutableStateOf(latestBuild()) }
146
- LaunchedEffect(Unit) {
147
- engine.editor.onHistoryUpdated().collect {
148
- items = latestBuild()
149
- }
150
- }
151
-
152
- if (items.isEmpty()) return
153
-
154
- val haptics = LocalHapticFeedback.current
155
- val scope = rememberCoroutineScope()
156
- val pagerState = rememberPagerState(pageCount = { items.size })
157
- // True only while the user is dragging the pager with their finger (not fling/programmatic).
158
- val isDragged by pagerState.interactionSource.collectIsDraggedAsState()
159
-
160
- // The item currently on screen: the last one that has reached its "becomes current" threshold
161
- // (crossfade midpoint for images, clip start for videos) — earlier than full visibility so the
162
- // strip tracks the incoming clip as it comes into view rather than lagging behind.
163
- val playbackIndex = items.indexOfLast { currentTime >= it.currentThreshold }.coerceAtLeast(0)
164
-
165
- // Seek live as the highlighted (centered) item changes under the user's finger, so playback
166
- // tracks the strip while dragging instead of only updating once they let go. A haptic marks
167
- // each change. Only while the finger is down (isDragged) — taps go through onClick instead.
168
- LaunchedEffect(Unit) {
169
- snapshotFlow { pagerState.currentPage }.collect { page ->
170
- if (isDragged && page in items.indices) {
171
- haptics.performHapticFeedback(HapticFeedbackType.LongPress)
172
- onSeekToTime(items[page].seekTarget)
173
- }
174
- }
175
- }
176
-
177
- // Settle on the highlighted item whenever the strip is idle — not under the finger, not mid-scroll.
178
- // Follows normal playback, centers a tapped item, and after a fling pulls any momentum overshoot
179
- // back to the item highlighted when the finger lifted (where live-seek left playback).
180
- // One long-lived collector (NOT a re-keyed effect): re-keying on isScrollInProgress would cancel
181
- // and restart the in-flight animateScrollToPage every frame, making the strip jitter back and forth.
182
- // playbackIndex is a plain (recomposed) val, so wrap it in a State the snapshotFlow can observe —
183
- // otherwise the flow only reacts to drag/scroll changes and never to a tap/seek changing the target.
184
- val targetIndex by rememberUpdatedState(playbackIndex)
185
- LaunchedEffect(Unit) {
186
- snapshotFlow { Triple(targetIndex, isDragged, pagerState.isScrollInProgress) }
187
- .collect { (target, dragging, scrolling) ->
188
- if (!dragging && !scrolling && target != pagerState.currentPage) {
189
- pagerState.animateScrollToPage(target)
190
- }
191
- }
192
- }
193
-
194
- BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
195
- // Pad each side by half the leftover width so the current page always snaps to the center,
196
- // including the first page at the start and the last page at the end.
197
- val sidePadding = ((maxWidth - ITEM_WIDTH) / 2f).coerceAtLeast(0.dp)
198
-
199
- HorizontalPager(
200
- state = pagerState,
201
- pageSize = PageSize.Fixed(ITEM_WIDTH),
202
- contentPadding = PaddingValues(horizontal = sidePadding),
203
- pageSpacing = 8.dp,
204
- modifier = Modifier
205
- .padding(vertical = 8.dp)
206
- .fillMaxWidth()
207
- .height(ITEM_HEIGHT)
208
- // Exclude the strip from the system back gesture so horizontal swipes scroll the
209
- // pager instead of accidentally navigating back.
210
- .systemGestureExclusion(),
211
- key = { items[it].startTime },
212
- ) { index ->
213
- val item = items[index]
214
- val overlayWindow = (item.endTime - item.currentThreshold).coerceAtLeast(0.0001)
215
- val playedFraction = when {
216
- currentTime >= item.endTime -> 1f
217
- currentTime <= item.currentThreshold -> 0f
218
- else -> ((currentTime - item.currentThreshold) / overlayWindow).toFloat().coerceIn(0f, 1f)
219
- }
220
-
221
- // Distance from the centered page (in pages), normalized so the edge item = 1f.
222
- val pageOffset =
223
- ((pagerState.currentPage - index) + pagerState.currentPageOffsetFraction).absoluteValue
224
- val itemsToEdge = ((maxWidth.value / 2f) / ITEM_WIDTH.value).coerceAtLeast(1f)
225
- val edgeFraction = (pageOffset / itemsToEdge).coerceIn(0f, 1f)
226
-
227
- ThumbnailItem(
228
- item = item,
229
- playedFraction = playedFraction,
230
- edgeFraction = edgeFraction,
231
- isCurrent = index == pagerState.currentPage,
232
- onClick = {
233
- // Scroll to center the tapped item directly (don't rely on the playback-follow
234
- // chain, which only reacts when the engine's playback time changes), and seek.
235
- scope.launch { pagerState.animateScrollToPage(index) }
236
- onSeekToTime(item.seekTarget)
237
- },
238
- )
239
- }
240
- }
241
- }
242
-
243
- @Composable
244
- private fun ThumbnailItem(
245
- item: StripItem,
246
- playedFraction: Float,
247
- edgeFraction: Float,
248
- isCurrent: Boolean,
249
- onClick: () -> Unit,
250
- ) {
251
- // Horizontal inset and fade scale with distance from center (0 at center → max at the edges).
252
- val horizontalPadding = 0.dp * edgeFraction
253
- val itemAlpha = 1f - (0.75f * edgeFraction) // 100% at center → 25% at the edges
254
-
255
- // Non-current items get a small animated vertical inset.
256
- val verticalPadding by animateDpAsState(
257
- targetValue = if (isCurrent) 0.dp else 8.dp,
258
- label = "thumb_v_padding",
259
- )
260
-
261
- Box(
262
- modifier = Modifier
263
- .fillMaxSize()
264
- .alpha(itemAlpha)
265
- .padding(horizontal = horizontalPadding, vertical = verticalPadding)
266
- .clip(RoundedCornerShape(8.dp))
267
- .clickable { onClick() },
268
- ) {
269
- if (item.url != null) {
270
- // For video, show a static middle frame; for images, load the URL directly.
271
- val model: Any? = if (item.isVideo) rememberVideoThumbnail(item.url) else item.url
272
- AsyncImage(
273
- model = model,
274
- contentDescription = null,
275
- modifier = Modifier.fillMaxSize(),
276
- contentScale = ContentScale.Crop,
277
- )
278
- } else {
279
- // Title item: a "T" icon instead of a picture.
280
- Box(
281
- modifier = Modifier
282
- .fillMaxSize()
283
- .background(MaterialTheme.colorScheme.primaryContainer),
284
- contentAlignment = Alignment.Center,
285
- ) {
286
- Icon(
287
- imageVector = IconPack.Title,
288
- contentDescription = "Title",
289
- tint = MaterialTheme.colorScheme.onPrimaryContainer,
290
- )
291
- }
292
- }
293
-
294
- // "Played" overlay: grows left→right; fully covers items already played.
295
- if (playedFraction > 0f) {
296
- Box(
297
- modifier = Modifier
298
- .align(Alignment.CenterStart)
299
- .fillMaxHeight()
300
- .fillMaxWidth(playedFraction)
301
- .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.28f)),
302
- )
303
- }
304
-
305
- // Play badge to mark video items.
306
- if (item.isVideo) {
307
- Box(
308
- modifier = Modifier
309
- .align(Alignment.Center)
310
- .size(20.dp)
311
- .background(Color.Black.copy(alpha = 0.45f), CircleShape),
312
- contentAlignment = Alignment.Center,
313
- ) {
314
- Icon(
315
- imageVector = IconPack.PlayArrow,
316
- contentDescription = null,
317
- tint = Color.White,
318
- modifier = Modifier.size(13.dp),
319
- )
320
- }
321
- }
322
- }
323
- }