@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.
- package/android/build.gradle +4 -1
- package/package.json +1 -1
- package/src/NativeIMGLYEditor.ts +1 -1
- package/android/src/main/assets/ly.img.memories.style/content.json +0 -50
- package/android/src/main/assets/ly.img.memories.style/thumbnails/bubblegum.png +0 -0
- package/android/src/main/assets/ly.img.memories.style/thumbnails/hologram.png +0 -0
- package/android/src/main/assets/ly.img.memories.style/thumbnails/noir.png +0 -0
- package/android/src/main/assets/ly.img.memories.style/videos/bubblegum.mp4 +0 -0
- package/android/src/main/assets/ly.img.memories.style/videos/hologram.mp4 +0 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesApp.kt +0 -77
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesConfiguration.kt +0 -38
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesEditor.kt +0 -79
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesViewModel.kt +0 -469
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/callback/OnCreate.kt +0 -69
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/callback/OnExport.kt +0 -46
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/BottomPanel.kt +0 -151
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/Dock.kt +0 -145
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/ExportOverlay.kt +0 -229
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/NavigationBar.kt +0 -80
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/Overlay.kt +0 -138
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/extension/EditorScopeExt.kt +0 -111
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/AddPhotoAlternate.kt +0 -79
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/ArrowForward.kt +0 -45
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Audiotrack.kt +0 -43
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Check.kt +0 -39
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/CheckCircle.kt +0 -44
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Close.kt +0 -43
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Delete.kt +0 -54
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/DeleteOutline.kt +0 -53
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Fullscreen.kt +0 -74
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Image.kt +0 -51
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Info.kt +0 -48
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Pause.kt +0 -46
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/PlayArrow.kt +0 -38
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/RepeatOff.kt +0 -80
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/RepeatOn.kt +0 -59
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/SkipPrevious.kt +0 -46
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Sort.kt +0 -55
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Style.kt +0 -59
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Title.kt +0 -43
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/TouchTriple.kt +0 -134
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/VolumeDown.kt +0 -48
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/VolumeUp.kt +0 -60
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/ExportStatus.kt +0 -20
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/ImageItem.kt +0 -8
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/TimelineImage.kt +0 -18
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Events.kt +0 -59
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Playback.kt +0 -29
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/SceneSetup.kt +0 -79
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Timeline.kt +0 -27
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Title.kt +0 -162
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/TrackEditor.kt +0 -740
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/screen/ImageSelectionScreen.kt +0 -387
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/screen/LoadingScreen.kt +0 -258
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/FilterStylesSheet.kt +0 -167
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/FilterStylesSheetType.kt +0 -21
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/VolumeSheet.kt +0 -104
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/StyleApplier.kt +0 -220
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/StyleAssetSource.kt +0 -43
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/VideoStyle.kt +0 -118
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Animations.kt +0 -58
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Constants.kt +0 -20
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/ImageDate.kt +0 -81
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Utils.kt +0 -27
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/CenteredAddImageButton.kt +0 -82
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/DraggableImageItem.kt +0 -145
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/FullscreenVideoControls.kt +0 -136
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageGrid.kt +0 -122
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageGridOverlay.kt +0 -149
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageTimelineStrip.kt +0 -323
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageToolbar.kt +0 -401
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ReorderableGridState.kt +0 -183
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/VideoControlPanel.kt +0 -180
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/VideoThumbnail.kt +0 -110
package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageGridOverlay.kt
DELETED
|
@@ -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
|
-
}
|
package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageTimelineStrip.kt
DELETED
|
@@ -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
|
-
}
|