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

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,58 +0,0 @@
1
- package ly.img.editor.configuration.memories.util
2
-
3
- import ly.img.engine.AnimationType
4
- import ly.img.engine.Engine
5
-
6
- /**
7
- * The slide animations for the Memories slideshow — **this is the file to edit**.
8
- *
9
- * Every photo/clip gets an in-animation (as it appears) and an out-animation (as it leaves),
10
- * bundled together as an [AnimationPair]. The kit picks one pair at random per slide
11
- * ([getRandomAnimationPair]).
12
- *
13
- * An [AnimationPair] is a *recipe*, not a pair of engine blocks: [AnimationPair.createIn] /
14
- * [AnimationPair.createOut] build a **fresh** engine animation each time they are called. Engine
15
- * animations are single-owner (1:1 with a design block), so every slide must own its own animation
16
- * instances — assigning one pooled animation block to several slides corrupts the scene (the engine
17
- * re-points the animation to the newest block only) and later crashes: destroying a slide auto-
18
- * destroys the shared animation, leaving every other slide holding a dangling id. [applySlideAnimation]
19
- * calls these builders per slide so each clip gets its own blocks.
20
- *
21
- * To change the motion between slides, edit the list in [createAnimationPairs] — add, remove, or
22
- * tweak entries (e.g. the `blurZoom` zoom strength), or write a whole new pair from two engine
23
- * animations. The low-level builders below wrap the engine calls so the list stays readable.
24
- */
25
- data class AnimationPair(
26
- val createIn: (Engine) -> Int,
27
- val createOut: (Engine) -> Int,
28
- )
29
-
30
- object Animations {
31
- /** The pool of animations a slide can use. Edit this list to change the slideshow's motion. */
32
- fun createAnimationPairs(): List<AnimationPair> = listOf(
33
- blurZoom(),
34
- )
35
-
36
- /** Pick a random pair for the next slide. */
37
- fun getRandomAnimationPair(animationPairs: List<AnimationPair>): AnimationPair = animationPairs.random()
38
-
39
- // ---- Low-level builders (you usually don't need to touch these) --------------------------
40
-
41
- /** Blur in, then crop-zoom out. [zoomScale] is how far the out-animation zooms (1.0 = none). */
42
- private fun blurZoom(zoomScale: Float = 1.7f): AnimationPair = AnimationPair(
43
- createIn = { engine ->
44
- engine.block.createAnimation(AnimationType.Blur).also { inAnimation ->
45
- engine.block.setDuration(inAnimation, IMAGE_DURATION * .5f)
46
- engine.block.setEnum(inAnimation, "animationEasing", "EaseOut")
47
- }
48
- },
49
- createOut = { engine ->
50
- engine.block.createAnimation(AnimationType.CropZoom).also { outAnimation ->
51
- engine.block.setDuration(outAnimation, IMAGE_DURATION * .5f)
52
- engine.block.setBoolean(outAnimation, "animation/crop_zoom/fade", true)
53
- engine.block.setEnum(outAnimation, "animationEasing", "EaseIn")
54
- engine.block.setFloat(outAnimation, "animation/crop_zoom/scale", zoomScale)
55
- }
56
- },
57
- )
58
- }
@@ -1,20 +0,0 @@
1
- package ly.img.editor.configuration.memories.util
2
-
3
- // The knobs for the slideshow. Tweak these to retime, resize, or restyle the montage.
4
-
5
- // Canvas — the slideshow page size. Portrait by default; square = 1080 x 1080, landscape = 1920 x 1080.
6
- const val PAGE_WIDTH = 1080f
7
- const val PAGE_HEIGHT = 1920f
8
-
9
- // Inset (dp) keeping the page off the screen edges and bottom controls.
10
- const val CANVAS_PADDING_DP = 10
11
-
12
- // Timing (seconds).
13
- const val IMAGE_DURATION = 8.0
14
- val OVERLAP_DURATION = IMAGE_DURATION * 0.4
15
- const val TITLE_DURATION = 3.0
16
-
17
- // Title card.
18
- const val TITLE_FONT_SIZE = 48f
19
- const val BURST_IMAGE_COUNT = 10 // images flashed behind the title
20
- const val BURST_TITLE_FRACTION = 0.9 // burst spans this fraction of the title duration
@@ -1,81 +0,0 @@
1
- package ly.img.editor.configuration.memories.util
2
-
3
- import android.content.ContentResolver
4
- import android.net.Uri
5
- import android.provider.MediaStore
6
- import androidx.exifinterface.media.ExifInterface
7
- import kotlinx.coroutines.Dispatchers
8
- import kotlinx.coroutines.withContext
9
- import ly.img.editor.configuration.memories.model.ImageItem
10
- import java.text.SimpleDateFormat
11
- import java.util.Locale
12
- import java.util.TimeZone
13
-
14
- /**
15
- * Build [ImageItem]s from freshly-picked [uris], assigning ids from [baseId] + 1 so they stay unique
16
- * even after deletions. Runs entirely on [Dispatchers.IO] because resolving MIME types and reading
17
- * EXIF dates hits the ContentResolver; callers dispatch the result back to the main thread.
18
- */
19
- suspend fun buildImageItems(
20
- contentResolver: ContentResolver,
21
- uris: List<Uri>,
22
- baseId: Int,
23
- ): List<ImageItem> = withContext(Dispatchers.IO) {
24
- uris.mapIndexed { index, uri ->
25
- val isVideo = contentResolver.getType(uri)?.startsWith("video/") == true
26
- ImageItem(
27
- id = baseId + index + 1,
28
- url = uri.toString(),
29
- isVideo = isVideo,
30
- creationDate = if (isVideo) null else getImageCreationDate(contentResolver, uri),
31
- )
32
- }
33
- }
34
-
35
- /**
36
- * Extract creation date from an image URI using MediaStore and EXIF data.
37
- * Returns timestamp in milliseconds since epoch, or null if not available.
38
- */
39
- fun getImageCreationDate(
40
- contentResolver: ContentResolver,
41
- uri: Uri,
42
- ): Long? {
43
- try {
44
- // First try to get DATE_TAKEN from MediaStore (most reliable for gallery images)
45
- val projection = arrayOf(MediaStore.Images.ImageColumns.DATE_TAKEN)
46
- contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
47
- if (cursor.moveToFirst()) {
48
- val dateIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN)
49
- val dateTaken = cursor.getLong(dateIndex)
50
- if (dateTaken > 0) {
51
- return dateTaken
52
- }
53
- }
54
- }
55
- } catch (e: Exception) {
56
- // MediaStore query failed, continue to EXIF fallback
57
- }
58
-
59
- try {
60
- // Fallback to EXIF data
61
- contentResolver.openInputStream(uri)?.use { inputStream ->
62
- val exif = ExifInterface(inputStream)
63
- val dateTimeOriginal = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)
64
- ?: exif.getAttribute(ExifInterface.TAG_DATETIME)
65
- ?: exif.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED)
66
-
67
- dateTimeOriginal?.let { dateString ->
68
- // EXIF DateTimeOriginal is local wall-clock time with no zone. Interpret it in the
69
- // device's zone so the epoch lines up with MediaStore's DATE_TAKEN (which we prefer
70
- // above); parsing it as UTC instead skews mixed-source sorting by the UTC offset.
71
- val sdf = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US)
72
- sdf.timeZone = TimeZone.getDefault()
73
- return sdf.parse(dateString)?.time
74
- }
75
- }
76
- } catch (e: Exception) {
77
- // EXIF extraction failed
78
- }
79
-
80
- return null
81
- }
@@ -1,27 +0,0 @@
1
- package ly.img.editor.configuration.memories.util
2
-
3
- import ly.img.editor.configuration.memories.model.ImageItem
4
-
5
- /** Sort images chronologically by creation date; images without a date sort last (asc)/first (desc). */
6
- fun sortByDate(
7
- images: List<ImageItem>,
8
- ascending: Boolean,
9
- ): List<ImageItem> = if (ascending) {
10
- images.sortedWith(compareBy<ImageItem> { it.creationDate ?: Long.MAX_VALUE }.thenBy { it.id })
11
- } else {
12
- images.sortedWith(compareByDescending<ImageItem> { it.creationDate ?: Long.MIN_VALUE }.thenByDescending { it.id })
13
- }
14
-
15
- /**
16
- * Detect whether [images] is already in chronological order.
17
- * Returns true for ascending (oldest first), false for descending (newest first),
18
- * or null if the list is unsorted (or too small / has no dates to be meaningfully ordered).
19
- */
20
- fun detectSortOrder(images: List<ImageItem>): Boolean? {
21
- if (images.size < 2) return null
22
- // Note: images without a creation date fall back to id order, so a freshly-added
23
- // batch still reads as "in order" (ascending).
24
- if (images == sortByDate(images, ascending = true)) return true
25
- if (images == sortByDate(images, ascending = false)) return false
26
- return null
27
- }
@@ -1,82 +0,0 @@
1
- package ly.img.editor.configuration.memories.widget
2
-
3
- import androidx.compose.foundation.background
4
- import androidx.compose.foundation.clickable
5
- import androidx.compose.foundation.layout.Arrangement
6
- import androidx.compose.foundation.layout.Box
7
- import androidx.compose.foundation.layout.Column
8
- import androidx.compose.foundation.layout.Spacer
9
- import androidx.compose.foundation.layout.fillMaxSize
10
- import androidx.compose.foundation.layout.height
11
- import androidx.compose.foundation.layout.offset
12
- import androidx.compose.foundation.layout.padding
13
- import androidx.compose.foundation.layout.size
14
- import androidx.compose.foundation.shape.RoundedCornerShape
15
- import androidx.compose.material3.Card
16
- import androidx.compose.material3.Icon
17
- import androidx.compose.material3.MaterialTheme
18
- import androidx.compose.material3.Text
19
- import androidx.compose.runtime.Composable
20
- import androidx.compose.ui.Alignment
21
- import androidx.compose.ui.Modifier
22
- import androidx.compose.ui.text.style.TextAlign
23
- import androidx.compose.ui.unit.IntOffset
24
- import androidx.compose.ui.unit.dp
25
- import ly.img.editor.configuration.memories.iconPack.AddPhotoAlternate
26
- import ly.img.editor.core.iconpack.IconPack
27
-
28
- /**
29
- * Empty-state call to action shown when no photos are selected yet:
30
- * a tappable card to add your media.
31
- */
32
- @Composable
33
- fun CenteredAddImageButton(
34
- onClick: () -> Unit,
35
- modifier: Modifier = Modifier,
36
- columnHeight: Float = 0f,
37
- ) {
38
- Box(
39
- modifier = modifier.fillMaxSize(),
40
- contentAlignment = Alignment.Center,
41
- ) {
42
- Column(
43
- modifier = Modifier.offset {
44
- IntOffset(0, columnHeight.toInt() / 2)
45
- },
46
- horizontalAlignment = Alignment.CenterHorizontally,
47
- ) {
48
- Card(
49
- modifier = Modifier
50
- .size(160.dp)
51
- .clickable { onClick() },
52
- shape = RoundedCornerShape(12.dp),
53
- ) {
54
- Column(
55
- modifier = Modifier
56
- .fillMaxSize()
57
- .background(
58
- MaterialTheme.colorScheme.surfaceVariant,
59
- RoundedCornerShape(12.dp),
60
- )
61
- .padding(16.dp),
62
- horizontalAlignment = Alignment.CenterHorizontally,
63
- verticalArrangement = Arrangement.Center,
64
- ) {
65
- Icon(
66
- imageVector = IconPack.AddPhotoAlternate,
67
- contentDescription = null,
68
- modifier = Modifier.size(48.dp),
69
- tint = MaterialTheme.colorScheme.onSurfaceVariant,
70
- )
71
- Spacer(modifier = Modifier.height(16.dp))
72
- Text(
73
- text = "Add your media",
74
- style = MaterialTheme.typography.bodyLarge,
75
- color = MaterialTheme.colorScheme.onSurfaceVariant,
76
- textAlign = TextAlign.Center,
77
- )
78
- }
79
- }
80
- }
81
- }
82
- }
@@ -1,145 +0,0 @@
1
- package ly.img.editor.configuration.memories.widget
2
-
3
- import androidx.compose.foundation.BorderStroke
4
- import androidx.compose.foundation.background
5
- import androidx.compose.foundation.border
6
- import androidx.compose.foundation.clickable
7
- import androidx.compose.foundation.layout.Box
8
- import androidx.compose.foundation.layout.aspectRatio
9
- import androidx.compose.foundation.layout.fillMaxSize
10
- import androidx.compose.foundation.layout.padding
11
- import androidx.compose.foundation.layout.size
12
- import androidx.compose.foundation.shape.CircleShape
13
- import androidx.compose.foundation.shape.RoundedCornerShape
14
- import androidx.compose.material3.Card
15
- import androidx.compose.material3.CardDefaults
16
- import androidx.compose.material3.Icon
17
- import androidx.compose.material3.MaterialTheme
18
- import androidx.compose.material3.Text
19
- import androidx.compose.runtime.Composable
20
- import androidx.compose.ui.Alignment
21
- import androidx.compose.ui.Modifier
22
- import androidx.compose.ui.graphics.Color
23
- import androidx.compose.ui.layout.ContentScale
24
- import androidx.compose.ui.text.font.FontWeight
25
- import androidx.compose.ui.unit.Dp
26
- import androidx.compose.ui.unit.dp
27
- import androidx.compose.ui.unit.sp
28
- import coil.compose.AsyncImage
29
- import ly.img.editor.configuration.memories.iconPack.CheckCircle
30
- import ly.img.editor.configuration.memories.iconPack.PlayArrow
31
- import ly.img.editor.configuration.memories.model.ImageItem
32
- import ly.img.editor.core.iconpack.IconPack
33
-
34
- @Composable
35
- fun DraggableImageItem(
36
- image: ImageItem,
37
- elevation: Dp,
38
- index: Int,
39
- isSelectedForDeletion: Boolean = false,
40
- onToggleSelection: (() -> Unit)? = null,
41
- ) {
42
- // The long-press-drag gesture is owned by the parent grid (ImageGrid); this item only renders.
43
- Card(
44
- modifier = Modifier
45
- .aspectRatio(4 / 5f)
46
- .then(
47
- if (onToggleSelection != null) {
48
- Modifier.clickable { onToggleSelection() }
49
- } else {
50
- Modifier
51
- },
52
- ),
53
- elevation = CardDefaults.cardElevation(defaultElevation = elevation),
54
- shape = RoundedCornerShape(8.dp),
55
- border = if (isSelectedForDeletion) {
56
- BorderStroke(3.dp, MaterialTheme.colorScheme.primary)
57
- } else {
58
- null
59
- },
60
- ) {
61
- Box {
62
- // For video, show a static middle frame; for images, load the URL directly.
63
- val model: Any? = if (image.isVideo) rememberVideoThumbnail(image.url) else image.url
64
- AsyncImage(
65
- model = model,
66
- contentDescription = if (image.isVideo) "Video ${image.id}" else "Image ${image.id}",
67
- modifier = Modifier.fillMaxSize(),
68
- contentScale = ContentScale.Crop,
69
- )
70
-
71
- // Play badge to mark video items.
72
- if (image.isVideo) {
73
- Box(
74
- modifier = Modifier
75
- .align(Alignment.Center)
76
- .size(34.dp)
77
- .background(Color.Black.copy(alpha = 0.45f), CircleShape),
78
- contentAlignment = Alignment.Center,
79
- ) {
80
- Icon(
81
- imageVector = IconPack.PlayArrow,
82
- contentDescription = null,
83
- tint = Color.White,
84
- modifier = Modifier.size(22.dp),
85
- )
86
- }
87
- }
88
-
89
- // Highlight scrim when marked for deletion
90
- if (isSelectedForDeletion) {
91
- Box(
92
- modifier = Modifier
93
- .fillMaxSize()
94
- .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.25f)),
95
- )
96
- }
97
-
98
- // Position index badge
99
- Box(
100
- modifier = Modifier
101
- .align(Alignment.BottomStart)
102
- .padding(4.dp)
103
- .background(
104
- Color.Black.copy(alpha = 0.7f),
105
- RoundedCornerShape(4.dp),
106
- )
107
- .padding(horizontal = 6.dp, vertical = 2.dp),
108
- ) {
109
- Text(
110
- text = "${index + 1}",
111
- color = Color.White,
112
- fontSize = 12.sp,
113
- fontWeight = FontWeight.Bold,
114
- )
115
- }
116
-
117
- // Selection checkbox (only when selection is enabled by the host)
118
- if (onToggleSelection != null) {
119
- Box(
120
- modifier = Modifier
121
- .align(Alignment.TopEnd)
122
- .padding(6.dp),
123
- ) {
124
- if (isSelectedForDeletion) {
125
- Icon(
126
- imageVector = IconPack.CheckCircle,
127
- contentDescription = "Selected for deletion",
128
- tint = MaterialTheme.colorScheme.primary,
129
- modifier = Modifier
130
- .size(24.dp)
131
- .background(Color.White, CircleShape),
132
- )
133
- } else {
134
- Box(
135
- modifier = Modifier
136
- .size(24.dp)
137
- .background(Color.Black.copy(alpha = 0.35f), CircleShape)
138
- .border(2.dp, Color.White, CircleShape),
139
- )
140
- }
141
- }
142
- }
143
- }
144
- }
145
- }
@@ -1,136 +0,0 @@
1
- package ly.img.editor.configuration.memories.widget
2
-
3
- import androidx.compose.foundation.background
4
- import androidx.compose.foundation.clickable
5
- import androidx.compose.foundation.interaction.MutableInteractionSource
6
- import androidx.compose.foundation.layout.Arrangement
7
- import androidx.compose.foundation.layout.Box
8
- import androidx.compose.foundation.layout.Row
9
- import androidx.compose.foundation.layout.fillMaxWidth
10
- import androidx.compose.foundation.layout.height
11
- import androidx.compose.foundation.layout.padding
12
- import androidx.compose.foundation.layout.size
13
- import androidx.compose.foundation.shape.CircleShape
14
- import androidx.compose.material.ripple.rememberRipple
15
- import androidx.compose.material3.ExperimentalMaterial3Api
16
- import androidx.compose.material3.Icon
17
- import androidx.compose.material3.Slider
18
- import androidx.compose.material3.SliderDefaults
19
- import androidx.compose.material3.Text
20
- import androidx.compose.runtime.Composable
21
- import androidx.compose.runtime.getValue
22
- import androidx.compose.runtime.mutableStateOf
23
- import androidx.compose.runtime.remember
24
- import androidx.compose.runtime.setValue
25
- import androidx.compose.ui.Alignment
26
- import androidx.compose.ui.Modifier
27
- import androidx.compose.ui.draw.clip
28
- import androidx.compose.ui.graphics.Brush
29
- import androidx.compose.ui.graphics.Color
30
- import androidx.compose.ui.tooling.preview.Preview
31
- import androidx.compose.ui.unit.dp
32
- import androidx.compose.ui.unit.sp
33
- import ly.img.editor.configuration.memories.iconPack.Pause
34
- import ly.img.editor.configuration.memories.iconPack.PlayArrow
35
- import ly.img.editor.core.iconpack.IconPack
36
-
37
- @OptIn(ExperimentalMaterial3Api::class)
38
- @Composable
39
- fun FullscreenVideoControls(
40
- isPlaying: Boolean,
41
- progress: Float,
42
- currentTime: Double,
43
- totalDuration: Double,
44
- onPlayPauseClick: () -> Unit,
45
- onSeek: (Float) -> Unit,
46
- modifier: Modifier = Modifier,
47
- ) {
48
- fun formatTime(seconds: Double): String {
49
- val totalSeconds = seconds.toInt().coerceAtLeast(0)
50
- return "%02d:%02d".format(totalSeconds / 60, totalSeconds % 60)
51
- }
52
-
53
- Box(
54
- modifier = modifier
55
- .fillMaxWidth()
56
- .height(80.dp)
57
- .background(
58
- brush = Brush.verticalGradient(
59
- colors = listOf(
60
- Color.Transparent,
61
- Color.Black.copy(alpha = 0.6f),
62
- ),
63
- ),
64
- ),
65
- ) {
66
- Row(
67
- modifier = Modifier
68
- .fillMaxWidth()
69
- .align(Alignment.BottomCenter)
70
- .padding(horizontal = 16.dp, vertical = 16.dp),
71
- horizontalArrangement = Arrangement.spacedBy(16.dp),
72
- verticalAlignment = Alignment.CenterVertically,
73
- ) {
74
- // Play/Pause Button
75
- Box(
76
- modifier = Modifier
77
- .size(48.dp)
78
- .clip(CircleShape)
79
- .clickable(
80
- interactionSource = remember { MutableInteractionSource() },
81
- indication = rememberRipple(
82
- bounded = false,
83
- radius = 28.dp,
84
- color = Color.White,
85
- ),
86
- ) { onPlayPauseClick() }
87
- .background(Color.Black.copy(alpha = 0.3f)),
88
- contentAlignment = Alignment.Center,
89
- ) {
90
- Icon(
91
- imageVector = if (isPlaying) IconPack.Pause else IconPack.PlayArrow,
92
- contentDescription = if (isPlaying) "Pause" else "Play",
93
- tint = Color.White,
94
- modifier = Modifier.size(32.dp),
95
- )
96
- }
97
-
98
- // Slider
99
- Slider(
100
- value = progress,
101
- onValueChange = { newValue ->
102
- onSeek(newValue)
103
- },
104
- colors = SliderDefaults.colors(
105
- thumbColor = Color.White,
106
- activeTrackColor = Color.White,
107
- inactiveTrackColor = Color.White.copy(alpha = 0.3f),
108
- ),
109
- modifier = Modifier.weight(1f),
110
- )
111
-
112
- // Current time over total duration, to the right of the slider
113
- Text(
114
- text = "${formatTime(currentTime)} / ${formatTime(totalDuration)}",
115
- color = Color.White,
116
- fontSize = 12.sp,
117
- )
118
- }
119
- }
120
- }
121
-
122
- @Preview(showBackground = true, backgroundColor = 0xFF1A1A1A)
123
- @Composable
124
- fun FullscreenVideoControlsPreview() {
125
- var isPlaying by remember { mutableStateOf(false) }
126
- var progress by remember { mutableStateOf(0.3f) }
127
-
128
- FullscreenVideoControls(
129
- isPlaying = isPlaying,
130
- progress = progress,
131
- currentTime = progress * 120.0,
132
- totalDuration = 120.0,
133
- onPlayPauseClick = { isPlaying = !isPlaying },
134
- onSeek = { progress = it },
135
- )
136
- }
@@ -1,122 +0,0 @@
1
- package ly.img.editor.configuration.memories.widget
2
-
3
- import androidx.compose.animation.core.animateDpAsState
4
- import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
5
- import androidx.compose.foundation.gestures.scrollBy
6
- import androidx.compose.foundation.layout.Arrangement
7
- import androidx.compose.foundation.layout.Box
8
- import androidx.compose.foundation.layout.PaddingValues
9
- import androidx.compose.foundation.layout.fillMaxHeight
10
- import androidx.compose.foundation.lazy.grid.GridCells
11
- import androidx.compose.foundation.lazy.grid.LazyGridState
12
- import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
13
- import androidx.compose.foundation.lazy.grid.items
14
- import androidx.compose.foundation.lazy.grid.rememberLazyGridState
15
- import androidx.compose.runtime.Composable
16
- import androidx.compose.runtime.LaunchedEffect
17
- import androidx.compose.runtime.getValue
18
- import androidx.compose.ui.Modifier
19
- import androidx.compose.ui.graphics.graphicsLayer
20
- import androidx.compose.ui.hapticfeedback.HapticFeedbackType
21
- import androidx.compose.ui.input.pointer.pointerInput
22
- import androidx.compose.ui.platform.LocalHapticFeedback
23
- import androidx.compose.ui.unit.dp
24
- import androidx.compose.ui.zIndex
25
- import kotlinx.coroutines.delay
26
- import kotlinx.coroutines.isActive
27
- import ly.img.editor.configuration.memories.model.ImageItem
28
-
29
- @Composable
30
- fun ImageGrid(
31
- modifier: Modifier = Modifier,
32
- images: List<ImageItem>,
33
- onImagesChanged: (List<ImageItem>) -> Unit,
34
- contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 72.dp),
35
- lazyGridState: LazyGridState = rememberLazyGridState(),
36
- selectedForDeletion: Set<Int> = emptySet(),
37
- onToggleSelection: ((Int) -> Unit)? = null,
38
- ) {
39
- // Empty state is owned by the caller (e.g. ImageSelectionScreen); render nothing here.
40
- if (images.isEmpty()) return
41
-
42
- val hapticFeedback = LocalHapticFeedback.current
43
-
44
- // Hand-written drag-reorder engine (no third-party reorder library). The slop gate inside
45
- // ReorderableGridState is what keeps the list from snapping when a top/bottom cell is picked up.
46
- val dragState = rememberReorderableGridState(lazyGridState) { from, to ->
47
- val newImages = images.toMutableList()
48
- newImages.add(to, newImages.removeAt(from))
49
- onImagesChanged(newImages)
50
-
51
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
52
- }
53
-
54
- // Continuous edge auto-scroll while dragging (only engages once the slop gate is satisfied).
55
- LaunchedEffect(dragState.isDragging) {
56
- if (!dragState.isDragging) return@LaunchedEffect
57
- while (isActive) {
58
- val speed = dragState.autoScrollSpeed
59
- if (speed != 0f) lazyGridState.scrollBy(speed)
60
- delay(16)
61
- }
62
- }
63
-
64
- LazyVerticalGrid(
65
- columns = GridCells.Fixed(3),
66
- state = lazyGridState,
67
- contentPadding = contentPadding,
68
- horizontalArrangement = Arrangement.spacedBy(8.dp),
69
- verticalArrangement = Arrangement.spacedBy(8.dp),
70
- modifier = modifier
71
- .fillMaxHeight()
72
- .pointerInput(dragState) {
73
- detectDragGesturesAfterLongPress(
74
- onDragStart = { offset ->
75
- dragState.onDragStart(offset)
76
- if (dragState.isDragging) {
77
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
78
- }
79
- },
80
- onDrag = { change, dragAmount ->
81
- change.consume()
82
- dragState.onDrag(dragAmount)
83
- },
84
- onDragEnd = dragState::onDragInterrupted,
85
- onDragCancel = dragState::onDragInterrupted,
86
- )
87
- },
88
- ) {
89
- items(images.size, key = { images[it].id }) { index ->
90
- val isDragging = index == dragState.draggingItemIndex
91
- val elevation by animateDpAsState(
92
- targetValue = if (isDragging) 8.dp else 0.dp,
93
- label = "elevation",
94
- )
95
-
96
- val imageId = images[index].id
97
- val itemModifier = if (isDragging) {
98
- // Dragged cell is placed manually; raise it and translate it under the finger.
99
- Modifier
100
- .zIndex(1f)
101
- .graphicsLayer {
102
- translationX = dragState.draggingItemOffset.x
103
- translationY = dragState.draggingItemOffset.y
104
- }
105
- } else {
106
- // Non-dragged cells keep their default placement. A sliding reorder animation would
107
- // need Modifier.animateItem() (Compose 1.7+); this module targets the older BOM.
108
- Modifier
109
- }
110
-
111
- Box(itemModifier) {
112
- DraggableImageItem(
113
- image = images[index],
114
- elevation = elevation,
115
- index = index,
116
- isSelectedForDeletion = imageId in selectedForDeletion,
117
- onToggleSelection = onToggleSelection?.let { toggle -> { toggle(imageId) } },
118
- )
119
- }
120
- }
121
- }
122
- }