@imgly/editor-react-native 1.80.0 → 1.81.0-rc.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 +1 -1
- package/android/src/main/assets/ly.img.memories.style/content.json +50 -0
- 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 +77 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesConfiguration.kt +38 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesEditor.kt +79 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesViewModel.kt +469 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/callback/OnCreate.kt +69 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/callback/OnExport.kt +46 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/BottomPanel.kt +151 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/Dock.kt +145 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/ExportOverlay.kt +229 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/NavigationBar.kt +80 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/Overlay.kt +138 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/extension/EditorScopeExt.kt +111 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/AddPhotoAlternate.kt +79 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/ArrowForward.kt +45 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Audiotrack.kt +43 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Check.kt +39 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/CheckCircle.kt +44 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Close.kt +43 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Delete.kt +54 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/DeleteOutline.kt +53 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Fullscreen.kt +74 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Image.kt +51 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Info.kt +48 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Pause.kt +46 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/PlayArrow.kt +38 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/RepeatOff.kt +80 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/RepeatOn.kt +59 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/SkipPrevious.kt +46 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Sort.kt +55 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Style.kt +59 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Title.kt +43 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/TouchTriple.kt +134 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/VolumeDown.kt +48 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/VolumeUp.kt +60 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/ExportStatus.kt +20 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/ImageItem.kt +8 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/TimelineImage.kt +18 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Events.kt +59 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Playback.kt +29 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/SceneSetup.kt +79 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Timeline.kt +27 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Title.kt +162 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/TrackEditor.kt +740 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/screen/ImageSelectionScreen.kt +387 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/screen/LoadingScreen.kt +258 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/FilterStylesSheet.kt +167 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/FilterStylesSheetType.kt +21 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/VolumeSheet.kt +104 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/StyleApplier.kt +220 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/StyleAssetSource.kt +43 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/VideoStyle.kt +118 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Animations.kt +58 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Constants.kt +20 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/ImageDate.kt +81 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Utils.kt +27 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/CenteredAddImageButton.kt +82 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/DraggableImageItem.kt +145 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/FullscreenVideoControls.kt +136 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageGrid.kt +122 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageGridOverlay.kt +149 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageTimelineStrip.kt +323 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageToolbar.kt +401 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ReorderableGridState.kt +183 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/VideoControlPanel.kt +180 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/VideoThumbnail.kt +110 -0
- package/android/src/main/kotlin/ly/img/editor/configuration/video/callback/OnCreate.kt +0 -7
- package/android/src/main/kotlin/ly/img/editor/configuration/video/component/InspectorBar.kt +2 -0
- package/ios/StarterKits/Video/callbacks/OnCreate+Video.swift +1 -1
- package/ios/StarterKits/Video/components/Dock+Video.swift +1 -0
- package/ios/StarterKits/Video/components/InspectorBar+Video.swift +4 -0
- package/package.json +1 -1
- package/src/NativeIMGLYEditor.ts +1 -1
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
package ly.img.editor.configuration.memories
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import androidx.lifecycle.ViewModel
|
|
6
|
+
import androidx.lifecycle.viewModelScope
|
|
7
|
+
import kotlinx.coroutines.Job
|
|
8
|
+
import kotlinx.coroutines.delay
|
|
9
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
10
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
11
|
+
import kotlinx.coroutines.flow.asStateFlow
|
|
12
|
+
import kotlinx.coroutines.launch
|
|
13
|
+
import ly.img.editor.configuration.memories.model.ImageItem
|
|
14
|
+
import ly.img.editor.configuration.memories.scene.applyDraftToTracks
|
|
15
|
+
import ly.img.editor.configuration.memories.scene.findTracks
|
|
16
|
+
import ly.img.editor.configuration.memories.scene.getPageVolume
|
|
17
|
+
import ly.img.editor.configuration.memories.scene.readImagesFromTracks
|
|
18
|
+
import ly.img.editor.configuration.memories.scene.setPageLooping
|
|
19
|
+
import ly.img.editor.configuration.memories.scene.setPageVolume
|
|
20
|
+
import ly.img.editor.configuration.memories.scene.togglePageLoop
|
|
21
|
+
import ly.img.editor.configuration.memories.style.VideoStyles
|
|
22
|
+
import ly.img.editor.configuration.memories.style.applyStyleToSlideshow
|
|
23
|
+
import ly.img.editor.configuration.memories.util.detectSortOrder
|
|
24
|
+
import ly.img.editor.configuration.memories.util.sortByDate
|
|
25
|
+
import ly.img.editor.core.EditorContext
|
|
26
|
+
import ly.img.editor.core.event.EditorEvent
|
|
27
|
+
import ly.img.editor.core.state.EditorViewMode
|
|
28
|
+
|
|
29
|
+
private const val TAG = "MemoriesVM"
|
|
30
|
+
|
|
31
|
+
class MemoriesViewModel : ViewModel() {
|
|
32
|
+
private var appContext: Context? = null
|
|
33
|
+
|
|
34
|
+
fun setContext(context: Context) {
|
|
35
|
+
appContext = context.applicationContext
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fun getContext(): Context = appContext
|
|
39
|
+
?: throw IllegalStateException("Context not set in ViewModel")
|
|
40
|
+
|
|
41
|
+
// Playback state
|
|
42
|
+
private val _isPlaying = MutableStateFlow(false)
|
|
43
|
+
val isPlaying: StateFlow<Boolean> = _isPlaying.asStateFlow()
|
|
44
|
+
|
|
45
|
+
// Selected images for the video
|
|
46
|
+
private val _selectedImages = MutableStateFlow<List<ImageItem>>(emptyList())
|
|
47
|
+
val selectedImages: StateFlow<List<ImageItem>> = _selectedImages.asStateFlow()
|
|
48
|
+
|
|
49
|
+
// Ids of images the user has marked for deletion (selection mode on the picker screen)
|
|
50
|
+
private val _imagesToDelete = MutableStateFlow<Set<Int>>(emptySet())
|
|
51
|
+
val imagesToDelete: StateFlow<Set<Int>> = _imagesToDelete.asStateFlow()
|
|
52
|
+
|
|
53
|
+
// Multi-select mode: when on, tiles show checkboxes and delete removes the selection;
|
|
54
|
+
// when off, delete clears all. Shared by the picker screen and the in-editor overlay.
|
|
55
|
+
private val _multiSelectMode = MutableStateFlow(false)
|
|
56
|
+
val multiSelectMode: StateFlow<Boolean> = _multiSelectMode.asStateFlow()
|
|
57
|
+
|
|
58
|
+
// Image grid state (used in overlay during editing)
|
|
59
|
+
private val _gridImages = MutableStateFlow<List<ImageItem>>(emptyList())
|
|
60
|
+
val gridImages: StateFlow<List<ImageItem>> = _gridImages.asStateFlow()
|
|
61
|
+
|
|
62
|
+
private val _showImageGrid = MutableStateFlow(false)
|
|
63
|
+
val showImageGrid: StateFlow<Boolean> = _showImageGrid.asStateFlow()
|
|
64
|
+
|
|
65
|
+
// Overlay (in-editor grid) staging state — edits are non-destructive until applied
|
|
66
|
+
private val _overlaySortEnabled = MutableStateFlow(false)
|
|
67
|
+
val overlaySortEnabled: StateFlow<Boolean> = _overlaySortEnabled.asStateFlow()
|
|
68
|
+
|
|
69
|
+
private val _overlaySortAscending = MutableStateFlow(true)
|
|
70
|
+
val overlaySortAscending: StateFlow<Boolean> = _overlaySortAscending.asStateFlow()
|
|
71
|
+
|
|
72
|
+
// Last filter style applied to image blocks, re-applied after an in-place track rebuild
|
|
73
|
+
private var activeFilter: String? = null
|
|
74
|
+
|
|
75
|
+
/** The currently applied style id (Default until one is chosen), so the Styles sheet preselects it. */
|
|
76
|
+
val activeStyleId: String get() = activeFilter ?: VideoStyles.DEFAULT.id
|
|
77
|
+
|
|
78
|
+
// Picker thumbnails (style id -> thumbUri), read once from the custom style asset source.
|
|
79
|
+
private val _styleThumbnails = MutableStateFlow<Map<String, String>>(emptyMap())
|
|
80
|
+
val styleThumbnails: StateFlow<Map<String, String>> = _styleThumbnails.asStateFlow()
|
|
81
|
+
|
|
82
|
+
/** Cache the style picker thumbnails resolved from the custom style asset source. */
|
|
83
|
+
fun setStyleThumbnails(thumbnails: Map<String, String>) {
|
|
84
|
+
_styleThumbnails.value = thumbnails
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Animation pairs
|
|
88
|
+
private val _animationPairs =
|
|
89
|
+
MutableStateFlow<List<ly.img.editor.configuration.memories.util.AnimationPair>>(emptyList())
|
|
90
|
+
val animationPairs: StateFlow<List<ly.img.editor.configuration.memories.util.AnimationPair>> =
|
|
91
|
+
_animationPairs.asStateFlow()
|
|
92
|
+
|
|
93
|
+
// Video title
|
|
94
|
+
private val _videoTitle = MutableStateFlow("")
|
|
95
|
+
val videoTitle: StateFlow<String> = _videoTitle.asStateFlow()
|
|
96
|
+
|
|
97
|
+
// Preview mode state management
|
|
98
|
+
private val _isPreviewMode = MutableStateFlow(false)
|
|
99
|
+
val isPreviewMode: StateFlow<Boolean> = _isPreviewMode.asStateFlow()
|
|
100
|
+
|
|
101
|
+
// State to control when dock/bottomPanel should be hidden during preview transition
|
|
102
|
+
private val _shouldHideUIForPreview = MutableStateFlow(false)
|
|
103
|
+
val shouldHideUIForPreview: StateFlow<Boolean> = _shouldHideUIForPreview.asStateFlow()
|
|
104
|
+
|
|
105
|
+
// Fullscreen mode state (for hiding system UI)
|
|
106
|
+
private val _isFullscreen = MutableStateFlow(false)
|
|
107
|
+
val isFullscreen: StateFlow<Boolean> = _isFullscreen.asStateFlow()
|
|
108
|
+
|
|
109
|
+
// Editor loading state
|
|
110
|
+
private val _isEditorLoading = MutableStateFlow(true)
|
|
111
|
+
val isEditorLoading: StateFlow<Boolean> = _isEditorLoading.asStateFlow()
|
|
112
|
+
|
|
113
|
+
// Loading start time for minimum duration calculation
|
|
114
|
+
private val _loadingStartTime = MutableStateFlow(0L)
|
|
115
|
+
val loadingStartTime: StateFlow<Long> = _loadingStartTime.asStateFlow()
|
|
116
|
+
|
|
117
|
+
// Loop state management
|
|
118
|
+
private val _isLooping = MutableStateFlow(true)
|
|
119
|
+
val isLooping: StateFlow<Boolean> = _isLooping.asStateFlow()
|
|
120
|
+
|
|
121
|
+
// Volume state management
|
|
122
|
+
private val _volume = MutableStateFlow(1.0f)
|
|
123
|
+
val volume: StateFlow<Float> = _volume.asStateFlow()
|
|
124
|
+
|
|
125
|
+
// Sort functionality
|
|
126
|
+
private val _isSortEnabled = MutableStateFlow(false)
|
|
127
|
+
val isSortEnabled: StateFlow<Boolean> = _isSortEnabled.asStateFlow()
|
|
128
|
+
|
|
129
|
+
private val _isSortAscending = MutableStateFlow(true)
|
|
130
|
+
val isSortAscending: StateFlow<Boolean> = _isSortAscending.asStateFlow()
|
|
131
|
+
|
|
132
|
+
// Editor context for direct editor control
|
|
133
|
+
private var editorContext: EditorContext? = null
|
|
134
|
+
|
|
135
|
+
// The in-flight "enter preview" transition, cancelled on the next toggle so a fast double-tap
|
|
136
|
+
// can't apply a stale view-mode change after the UI state has flipped back.
|
|
137
|
+
private var previewJob: Job? = null
|
|
138
|
+
|
|
139
|
+
// Functions to update state
|
|
140
|
+
fun setPlaying(playing: Boolean) {
|
|
141
|
+
_isPlaying.value = playing
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fun updateGridImages(images: List<ImageItem>) {
|
|
145
|
+
_gridImages.value = images
|
|
146
|
+
// Reflect the new order in the overlay sort button (a manual reorder turns it off).
|
|
147
|
+
refreshOverlaySortState()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fun updateSelectedImages(images: List<ImageItem>) {
|
|
151
|
+
_selectedImages.value = images
|
|
152
|
+
// Also update grid images to match selected images initially
|
|
153
|
+
_gridImages.value = images
|
|
154
|
+
// Reflect the new order in the sort button: enabled (with direction) when the list is in
|
|
155
|
+
// chronological order, off otherwise — so a manual reorder out of order turns it off.
|
|
156
|
+
refreshSortState()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Append newly-picked images to the selection. When sort is currently enabled the combined list
|
|
161
|
+
* is re-sorted so the additions land in chronological order; otherwise they are appended as-is.
|
|
162
|
+
* Either way [updateSelectedImages] then reconciles the sort button with the resulting order.
|
|
163
|
+
*/
|
|
164
|
+
fun addSelectedImages(newImages: List<ImageItem>) {
|
|
165
|
+
if (newImages.isEmpty()) return
|
|
166
|
+
val combined = _selectedImages.value + newImages
|
|
167
|
+
val next = if (_isSortEnabled.value) sortByDate(combined, _isSortAscending.value) else combined
|
|
168
|
+
updateSelectedImages(next)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fun setAnimationPairs(pairs: List<ly.img.editor.configuration.memories.util.AnimationPair>) {
|
|
172
|
+
_animationPairs.value = pairs
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fun setVideoTitle(title: String) {
|
|
176
|
+
_videoTitle.value = title
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fun togglePreviewMode() {
|
|
180
|
+
val currentPreviewMode = _isPreviewMode.value
|
|
181
|
+
// Cancel any in-flight enter-preview transition before starting/reverting one.
|
|
182
|
+
previewJob?.cancel()
|
|
183
|
+
if (!currentPreviewMode) {
|
|
184
|
+
// Entering preview mode: hide UI first, then enable preview after animation delay
|
|
185
|
+
_shouldHideUIForPreview.value = true
|
|
186
|
+
_isPreviewMode.value = true
|
|
187
|
+
_isFullscreen.value = true // Hide system UI
|
|
188
|
+
|
|
189
|
+
// Handle delayed preview mode transition in ViewModel scope
|
|
190
|
+
previewJob = viewModelScope.launch {
|
|
191
|
+
// Guarded like every other engine access here: the editor may be disposed inside the
|
|
192
|
+
// 200ms window, so never let it throw uncaught in viewModelScope.
|
|
193
|
+
runCatching {
|
|
194
|
+
editorContext?.let { context ->
|
|
195
|
+
context.engine.block.findAllSelected().forEach { selectedBlock ->
|
|
196
|
+
context.engine.block.setSelected(selectedBlock, false)
|
|
197
|
+
}
|
|
198
|
+
context.eventHandler.send(EditorEvent.Sheet.Close(false))
|
|
199
|
+
delay(200) // Animation duration for dock/bottomPanel
|
|
200
|
+
// Only enter preview if we are still meant to be in it (the cancel above
|
|
201
|
+
// usually covers a toggle-back, but re-check to be safe).
|
|
202
|
+
if (_isPreviewMode.value) {
|
|
203
|
+
context.eventHandler.send(EditorEvent.SetViewMode(EditorViewMode.Preview()))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}.onFailure { Log.w(TAG, "Could not enter preview mode", it) }
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
// Exiting preview mode: disable preview immediately, show UI after
|
|
210
|
+
_isPreviewMode.value = false
|
|
211
|
+
_shouldHideUIForPreview.value = false
|
|
212
|
+
_isFullscreen.value = false // Show system UI
|
|
213
|
+
|
|
214
|
+
// Set editor to edit mode immediately
|
|
215
|
+
runCatching {
|
|
216
|
+
editorContext?.eventHandler?.send(EditorEvent.SetViewMode(EditorViewMode.Edit()))
|
|
217
|
+
}.onFailure { Log.w(TAG, "Could not exit preview mode", it) }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fun setEditorLoading(loading: Boolean) {
|
|
222
|
+
if (loading) {
|
|
223
|
+
_loadingStartTime.value = System.currentTimeMillis()
|
|
224
|
+
}
|
|
225
|
+
_isEditorLoading.value = loading
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fun toggleSort() {
|
|
229
|
+
if (_isSortEnabled.value) {
|
|
230
|
+
// Already in chronological order — flip the direction.
|
|
231
|
+
_isSortAscending.value = !_isSortAscending.value
|
|
232
|
+
} else {
|
|
233
|
+
// List is out of order (e.g. after a manual drag): restore the last direction first
|
|
234
|
+
// rather than jumping to the opposite one. A second tap then flips it.
|
|
235
|
+
_isSortEnabled.value = true
|
|
236
|
+
}
|
|
237
|
+
applySortToImages()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fun clearSelectedImages() {
|
|
241
|
+
_selectedImages.value = emptyList()
|
|
242
|
+
_gridImages.value = emptyList()
|
|
243
|
+
_imagesToDelete.value = emptySet()
|
|
244
|
+
_isSortEnabled.value = false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Toggle multi-select mode; either transition resets the current selection to empty. */
|
|
248
|
+
fun toggleMultiSelect() {
|
|
249
|
+
_multiSelectMode.value = !_multiSelectMode.value
|
|
250
|
+
_imagesToDelete.value = emptySet()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Toggle whether an image (by id) is marked for deletion. */
|
|
254
|
+
fun toggleImageForDeletion(id: Int) {
|
|
255
|
+
_imagesToDelete.value = _imagesToDelete.value.toMutableSet().apply {
|
|
256
|
+
if (!add(id)) remove(id)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Remove only the images currently marked for deletion. */
|
|
261
|
+
fun deleteSelectedImages() {
|
|
262
|
+
val toDelete = _imagesToDelete.value
|
|
263
|
+
if (toDelete.isEmpty()) return
|
|
264
|
+
val remaining = _selectedImages.value.filterNot { it.id in toDelete }
|
|
265
|
+
_selectedImages.value = remaining
|
|
266
|
+
_gridImages.value = remaining
|
|
267
|
+
_imagesToDelete.value = emptySet()
|
|
268
|
+
// Removing images can leave (or break) chronological order — keep the button in sync.
|
|
269
|
+
refreshSortState()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ---- In-editor overlay grid: non-destructive staging ---------------------------------------
|
|
273
|
+
|
|
274
|
+
// ──────────────── In-editor overlay (non-destructive staging) ────────────────
|
|
275
|
+
|
|
276
|
+
/** Open the overlay seeded from the tracks (the source of truth) and pause playback. */
|
|
277
|
+
fun openImageGridFromTracks(editorContext: EditorContext) {
|
|
278
|
+
val engine = editorContext.engine
|
|
279
|
+
val page = engine.scene.getCurrentPage() ?: return
|
|
280
|
+
val tracks = findTracks(engine, page) ?: return
|
|
281
|
+
_gridImages.value = try {
|
|
282
|
+
readImagesFromTracks(engine, tracks)
|
|
283
|
+
} catch (e: Exception) {
|
|
284
|
+
Log.e(TAG, "Error reading images from tracks", e)
|
|
285
|
+
_gridImages.value
|
|
286
|
+
}
|
|
287
|
+
_imagesToDelete.value = emptySet()
|
|
288
|
+
_multiSelectMode.value = false
|
|
289
|
+
// Reflect the existing track order: if the images are already chronological, show it.
|
|
290
|
+
_overlaySortAscending.value = true
|
|
291
|
+
refreshOverlaySortState()
|
|
292
|
+
_showImageGrid.value = true
|
|
293
|
+
try {
|
|
294
|
+
engine.scene.getCurrentPage()?.let {
|
|
295
|
+
engine.block.setPlaying(it, false)
|
|
296
|
+
_isPlaying.value = false
|
|
297
|
+
}
|
|
298
|
+
} catch (e: Exception) {
|
|
299
|
+
Log.e(TAG, "Error pausing playback when opening overlay", e)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Close the overlay discarding the staged edits (tracks are untouched). */
|
|
304
|
+
fun cancelOverlay() {
|
|
305
|
+
_showImageGrid.value = false
|
|
306
|
+
_imagesToDelete.value = emptySet()
|
|
307
|
+
_multiSelectMode.value = false
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Sort the staged overlay draft by creation date (does not touch the tracks until applied). */
|
|
311
|
+
fun toggleOverlaySort() {
|
|
312
|
+
if (_overlaySortEnabled.value) {
|
|
313
|
+
// Already in chronological order — flip the direction.
|
|
314
|
+
_overlaySortAscending.value = !_overlaySortAscending.value
|
|
315
|
+
} else {
|
|
316
|
+
// Out of order (e.g. after a manual drag): restore the last direction first.
|
|
317
|
+
_overlaySortEnabled.value = true
|
|
318
|
+
}
|
|
319
|
+
_gridImages.value = sortByDate(_gridImages.value, _overlaySortAscending.value)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Append newly-picked images to the draft. */
|
|
323
|
+
fun addImagesToOverlay(images: List<ImageItem>) {
|
|
324
|
+
if (images.isEmpty()) return
|
|
325
|
+
val combined = _gridImages.value + images
|
|
326
|
+
// Keep chronological order when sort is on; otherwise just append.
|
|
327
|
+
_gridImages.value =
|
|
328
|
+
if (_overlaySortEnabled.value) sortByDate(combined, _overlaySortAscending.value) else combined
|
|
329
|
+
refreshOverlaySortState()
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Remove the overlay-selected images from the draft only. */
|
|
333
|
+
fun deleteSelectedFromOverlay() {
|
|
334
|
+
val toDelete = _imagesToDelete.value
|
|
335
|
+
if (toDelete.isEmpty()) return
|
|
336
|
+
_gridImages.value = _gridImages.value.filterNot { it.id in toDelete }
|
|
337
|
+
_imagesToDelete.value = emptySet()
|
|
338
|
+
// Removing images can leave (or break) chronological order — keep the button in sync.
|
|
339
|
+
refreshOverlaySortState()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Clear all images from the draft only. */
|
|
343
|
+
fun clearOverlayImages() {
|
|
344
|
+
_gridImages.value = emptyList()
|
|
345
|
+
_imagesToDelete.value = emptySet()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Apply the staged draft to the tracks in-place, then close the overlay. */
|
|
349
|
+
fun applyOverlayChanges(editorContext: EditorContext) {
|
|
350
|
+
val engine = editorContext.engine
|
|
351
|
+
viewModelScope.launch {
|
|
352
|
+
try {
|
|
353
|
+
val page = engine.scene.getCurrentPage() ?: return@launch
|
|
354
|
+
val tracks = findTracks(engine, page) ?: return@launch
|
|
355
|
+
val draft = _gridImages.value
|
|
356
|
+
applyDraftToTracks(
|
|
357
|
+
engine = engine,
|
|
358
|
+
page = page,
|
|
359
|
+
tracks = tracks,
|
|
360
|
+
staged = draft,
|
|
361
|
+
animationPairs = _animationPairs.value,
|
|
362
|
+
hasTitle = _videoTitle.value.isNotEmpty(),
|
|
363
|
+
)
|
|
364
|
+
// New blocks default to full-bleed and no effects; re-apply the active style so they
|
|
365
|
+
// match (including Default, which resets scale to full-bleed and clears the backdrop).
|
|
366
|
+
applyStyle(editorContext, activeStyleId)
|
|
367
|
+
_selectedImages.value = draft
|
|
368
|
+
_imagesToDelete.value = emptySet()
|
|
369
|
+
_multiSelectMode.value = false
|
|
370
|
+
_showImageGrid.value = false
|
|
371
|
+
} catch (e: Exception) {
|
|
372
|
+
Log.e(TAG, "Error applying overlay changes", e)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private fun applySortToImages() {
|
|
378
|
+
if (_isSortEnabled.value) {
|
|
379
|
+
val sortedImages = sortByDate(_selectedImages.value, _isSortAscending.value)
|
|
380
|
+
_selectedImages.value = sortedImages
|
|
381
|
+
_gridImages.value = sortedImages
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Reconcile the picker's sort state with the current order of [_selectedImages]. When the list
|
|
387
|
+
* happens to be in chronological order the sort button reflects that direction; when it is out
|
|
388
|
+
* of order sort turns off but the last direction is remembered (so the next tap restores it).
|
|
389
|
+
*/
|
|
390
|
+
private fun refreshSortState() {
|
|
391
|
+
val order = detectSortOrder(_selectedImages.value)
|
|
392
|
+
if (order == null) {
|
|
393
|
+
_isSortEnabled.value = false // keep _isSortAscending so the next tap restores this direction
|
|
394
|
+
} else {
|
|
395
|
+
_isSortEnabled.value = true
|
|
396
|
+
_isSortAscending.value = order
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Same reconciliation as [refreshSortState] but for the in-editor overlay draft. */
|
|
401
|
+
private fun refreshOverlaySortState() {
|
|
402
|
+
val order = detectSortOrder(_gridImages.value)
|
|
403
|
+
if (order == null) {
|
|
404
|
+
_overlaySortEnabled.value = false
|
|
405
|
+
} else {
|
|
406
|
+
_overlaySortEnabled.value = true
|
|
407
|
+
_overlaySortAscending.value = order
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
fun setEditorContext(context: EditorContext) {
|
|
412
|
+
editorContext = context
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Drop the editor context when the editor closes, so post-close calls no-op instead of acting
|
|
416
|
+
* on a stale/disposed editor. */
|
|
417
|
+
fun clearEditorContext() {
|
|
418
|
+
previewJob?.cancel()
|
|
419
|
+
editorContext = null
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Loop on, volume synced — called once the scene is built. */
|
|
423
|
+
fun initializeEditorState() {
|
|
424
|
+
val engine = editorContext?.engine ?: return
|
|
425
|
+
viewModelScope.launch {
|
|
426
|
+
runCatching {
|
|
427
|
+
setPageLooping(engine, true)
|
|
428
|
+
_isLooping.value = true
|
|
429
|
+
getPageVolume(engine)?.let { _volume.value = it }
|
|
430
|
+
}.onFailure { Log.w(TAG, "Could not initialize editor state", it) }
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ──────────────── Playback: loop & volume ────────────────
|
|
435
|
+
|
|
436
|
+
fun toggleLoop() {
|
|
437
|
+
val engine = editorContext?.engine ?: return
|
|
438
|
+
viewModelScope.launch {
|
|
439
|
+
runCatching { togglePageLoop(engine)?.let { _isLooping.value = it } }
|
|
440
|
+
.onFailure { Log.w(TAG, "Could not toggle loop", it) }
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
fun setVolume(volume: Float) {
|
|
445
|
+
val engine = editorContext?.engine ?: return
|
|
446
|
+
viewModelScope.launch {
|
|
447
|
+
runCatching {
|
|
448
|
+
setPageVolume(engine, volume)
|
|
449
|
+
_volume.value = volume
|
|
450
|
+
}.onFailure { Log.w(TAG, "Could not set volume", it) }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ──────────────── Styles ────────────────
|
|
455
|
+
|
|
456
|
+
/** Apply a style to the slideshow (see [applyStyleToSlideshow]); remembered for re-applies. */
|
|
457
|
+
fun applyStyle(
|
|
458
|
+
editorContext: EditorContext,
|
|
459
|
+
styleId: String,
|
|
460
|
+
) {
|
|
461
|
+
activeFilter = styleId
|
|
462
|
+
val engine = editorContext.engine
|
|
463
|
+
viewModelScope.launch {
|
|
464
|
+
val page = engine.scene.getCurrentPage() ?: return@launch
|
|
465
|
+
runCatching { applyStyleToSlideshow(engine, page, VideoStyles.byId(styleId)) }
|
|
466
|
+
.onFailure { Log.w(TAG, "Could not apply style '$styleId'", it) }
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package ly.img.editor.configuration.memories.callback
|
|
2
|
+
|
|
3
|
+
import androidx.compose.ui.unit.dp
|
|
4
|
+
import kotlinx.coroutines.flow.first
|
|
5
|
+
import ly.img.editor.configuration.memories.MemoriesConfiguration
|
|
6
|
+
import ly.img.editor.configuration.memories.MemoriesViewModel
|
|
7
|
+
import ly.img.editor.configuration.memories.extension.onLoadAssetSources
|
|
8
|
+
import ly.img.editor.configuration.memories.scene.calculateTotalDuration
|
|
9
|
+
import ly.img.editor.configuration.memories.scene.createBurstImages
|
|
10
|
+
import ly.img.editor.configuration.memories.scene.createMainImageSequence
|
|
11
|
+
import ly.img.editor.configuration.memories.scene.createSlideshowScene
|
|
12
|
+
import ly.img.editor.configuration.memories.scene.createTitleBlock
|
|
13
|
+
import ly.img.editor.configuration.memories.scene.setupEventSubscriptions
|
|
14
|
+
import ly.img.editor.configuration.memories.scene.setupTracks
|
|
15
|
+
import ly.img.editor.configuration.memories.style.loadStyleThumbnails
|
|
16
|
+
import ly.img.editor.configuration.memories.util.Animations
|
|
17
|
+
import ly.img.editor.configuration.memories.util.CANVAS_PADDING_DP
|
|
18
|
+
import ly.img.editor.core.EditorScope
|
|
19
|
+
import ly.img.editor.core.component.data.Insets
|
|
20
|
+
import ly.img.editor.core.event.EditorEvent
|
|
21
|
+
|
|
22
|
+
private fun EditorScope.applyCanvasPadding() {
|
|
23
|
+
editorContext.eventHandler.send(EditorEvent.Insets.SetExtra(insets = Insets(CANVAS_PADDING_DP.dp)))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fun MemoriesConfiguration.onCreateConfiguration(viewModel: MemoriesViewModel): (suspend EditorScope.() -> Unit) = onCreate@{
|
|
27
|
+
val engine = editorContext.engine
|
|
28
|
+
showLoading = true
|
|
29
|
+
viewModel.setEditorContext(editorContext)
|
|
30
|
+
|
|
31
|
+
val page = createSlideshowScene(engine)
|
|
32
|
+
onLoadAssetSources()
|
|
33
|
+
// Resolve the style picker thumbnails from the custom asset source now that it is registered.
|
|
34
|
+
viewModel.setStyleThumbnails(engine.loadStyleThumbnails())
|
|
35
|
+
|
|
36
|
+
// Without this the engine flattens our multi-track crossfade timeline on snapshot-restore
|
|
37
|
+
// (e.g. undo): it strips "foreground" tracks and reparents their clips to the page. Must be
|
|
38
|
+
// set after the scene exists.
|
|
39
|
+
engine.editor.setSettingBoolean("features/removeForegroundTracksOnSceneLoad", false)
|
|
40
|
+
|
|
41
|
+
val tracks = setupTracks(engine, page)
|
|
42
|
+
|
|
43
|
+
val animationPairs = Animations.createAnimationPairs()
|
|
44
|
+
viewModel.setAnimationPairs(animationPairs)
|
|
45
|
+
|
|
46
|
+
val images = viewModel.gridImages.first()
|
|
47
|
+
val hasTitle = createTitleBlock(engine, page, viewModel.videoTitle.first(), tracks.textTrack)
|
|
48
|
+
createBurstImages(engine, page, runCatching { viewModel.getContext() }.getOrNull(), images, hasTitle)
|
|
49
|
+
|
|
50
|
+
// One track per slide, positioned by its time offset (which already includes the title
|
|
51
|
+
// offset when a title is present), each appended on top of the previous for the crossfade.
|
|
52
|
+
// Returns each slot's on-timeline duration (a short video is only as long as its source).
|
|
53
|
+
val durations = createMainImageSequence(engine, page, images, hasTitle, animationPairs)
|
|
54
|
+
|
|
55
|
+
val totalDuration = calculateTotalDuration(images, durations, hasTitle)
|
|
56
|
+
engine.block.setDuration(page, duration = totalDuration)
|
|
57
|
+
engine.block.setDuration(tracks.backgroundBlock, duration = totalDuration)
|
|
58
|
+
engine.block.setDuration(tracks.matteBlock, duration = totalDuration)
|
|
59
|
+
|
|
60
|
+
engine.block.fillParent(tracks.backgroundTrack)
|
|
61
|
+
engine.block.fillParent(tracks.matteTrack)
|
|
62
|
+
engine.block.fillParent(tracks.textTrack)
|
|
63
|
+
|
|
64
|
+
viewModel.initializeEditorState()
|
|
65
|
+
showLoading = false
|
|
66
|
+
|
|
67
|
+
setupEventSubscriptions(engine, editorContext, viewModel)
|
|
68
|
+
applyCanvasPadding()
|
|
69
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package ly.img.editor.configuration.memories.callback
|
|
2
|
+
|
|
3
|
+
import kotlinx.coroutines.CancellationException
|
|
4
|
+
import ly.img.editor.configuration.memories.MemoriesConfiguration
|
|
5
|
+
import ly.img.editor.configuration.memories.model.ExportStatus
|
|
6
|
+
import ly.img.engine.MimeType
|
|
7
|
+
import java.nio.ByteBuffer
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Render the slideshow page to an MP4, reporting progress through [MemoriesConfiguration.exportStatus]
|
|
11
|
+
* so the overlay can show a progress circle, then a share button on success.
|
|
12
|
+
*/
|
|
13
|
+
suspend fun MemoriesConfiguration.onExport() {
|
|
14
|
+
try {
|
|
15
|
+
exportStatus = ExportStatus.Loading(progress = 0f)
|
|
16
|
+
val buffer = exportSlideshow()
|
|
17
|
+
val file = writeToFile(byteBuffer = buffer, mimeType = MimeType.MP4)
|
|
18
|
+
exportStatus = ExportStatus.Success(file = file, mimeType = MimeType.MP4)
|
|
19
|
+
} catch (cancellation: CancellationException) {
|
|
20
|
+
exportStatus = null
|
|
21
|
+
throw cancellation
|
|
22
|
+
} catch (exception: Exception) {
|
|
23
|
+
exportStatus = ExportStatus.Error(exception)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private suspend fun MemoriesConfiguration.exportSlideshow(): ByteBuffer {
|
|
28
|
+
val engine = editorContext.engine
|
|
29
|
+
val page = requireNotNull(engine.scene.getCurrentPage())
|
|
30
|
+
return engine.block.exportVideo(
|
|
31
|
+
block = page,
|
|
32
|
+
timeOffset = 0.0,
|
|
33
|
+
duration = engine.block.getDuration(page),
|
|
34
|
+
mimeType = MimeType.MP4,
|
|
35
|
+
progressCallback = { progress ->
|
|
36
|
+
if (progress.totalFrames > 0) {
|
|
37
|
+
val fraction = progress.encodedFrames.toFloat() / progress.totalFrames
|
|
38
|
+
val current = exportStatus
|
|
39
|
+
// Only push a new state on a visible (~1%) change to avoid churning recomposition.
|
|
40
|
+
if (current !is ExportStatus.Loading || fraction >= current.progress + 0.01f) {
|
|
41
|
+
exportStatus = ExportStatus.Loading(progress = fraction)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
}
|