@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.
- 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
|
@@ -1,740 +0,0 @@
|
|
|
1
|
-
package ly.img.editor.configuration.memories.scene
|
|
2
|
-
|
|
3
|
-
import android.util.Log
|
|
4
|
-
import ly.img.editor.configuration.memories.model.ImageItem
|
|
5
|
-
import ly.img.editor.configuration.memories.model.TimelineImage
|
|
6
|
-
import ly.img.editor.configuration.memories.util.AnimationPair
|
|
7
|
-
import ly.img.editor.configuration.memories.util.Animations
|
|
8
|
-
import ly.img.editor.configuration.memories.util.IMAGE_DURATION
|
|
9
|
-
import ly.img.editor.configuration.memories.util.OVERLAP_DURATION
|
|
10
|
-
import ly.img.editor.configuration.memories.util.TITLE_DURATION
|
|
11
|
-
import ly.img.engine.DesignBlock
|
|
12
|
-
import ly.img.engine.DesignBlockType
|
|
13
|
-
import ly.img.engine.Engine
|
|
14
|
-
import ly.img.engine.FillType
|
|
15
|
-
import ly.img.engine.ShapeType
|
|
16
|
-
import kotlin.math.abs
|
|
17
|
-
import kotlin.math.min
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The tracks are the single source of truth for the slideshow. Each photo/clip lives alone on its
|
|
21
|
-
* own track, positioned in time by the track's time offset (see [slotStartTime]); tracks are
|
|
22
|
-
* appended to the page in slot order, so a later slide always renders **above** the earlier one and
|
|
23
|
-
* every crossfade reads as "the incoming clip fades in over the outgoing one".
|
|
24
|
-
*
|
|
25
|
-
* Read the current media back from the tracks with [readImagesFromTracks] / [readTimelineImages];
|
|
26
|
-
* stage an edit in-place with [applyDraftToTracks]. Block ids are never retained (the engine
|
|
27
|
-
* invalidates them) — everything is re-located by the role/index metadata written here.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
private const val TAG = "MemoriesTracks"
|
|
31
|
-
|
|
32
|
-
// ---- Metadata keys -----------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
const val IMAGE_INDEX_KEY = "index" // slot order, also used by filter/animation code
|
|
35
|
-
private const val IMG_ID_KEY = "imgId" // stable UI id so the grid survives reordering
|
|
36
|
-
private const val CREATION_DATE_KEY = "creationDate"
|
|
37
|
-
private const val MEDIA_TYPE_KEY = "mediaType" // "image" (default) or "video"
|
|
38
|
-
private const val MEDIA_TYPE_VIDEO = "video"
|
|
39
|
-
|
|
40
|
-
const val SPACER_TYPE_KEY = "spacerType"
|
|
41
|
-
const val SPACER_BETWEEN = "between" // generic spacer (used by the burst-image intro)
|
|
42
|
-
|
|
43
|
-
// Track roles, tagged on the slideshow tracks so they can be re-located by metadata instead of
|
|
44
|
-
// retaining (engine-invalidated) block ids. See [tagSlideshowTracks] / [createMediaTrack] /
|
|
45
|
-
// [findTracks]. Each media clip gets its own track tagged [TRACK_ROLE_MEDIA].
|
|
46
|
-
private const val TRACK_ROLE_KEY = "trackRole"
|
|
47
|
-
private const val TRACK_ROLE_MEDIA = "media"
|
|
48
|
-
private const val TRACK_ROLE_TEXT = "text"
|
|
49
|
-
private const val TRACK_ROLE_BACKGROUND = "background"
|
|
50
|
-
private const val TRACK_ROLE_MATTE = "matte"
|
|
51
|
-
|
|
52
|
-
private const val GRAPHIC_TYPE = "//ly.img.ubq/graphic"
|
|
53
|
-
private const val TEXT_TYPE = "//ly.img.ubq/text"
|
|
54
|
-
private const val TRACK_TYPE = "//ly.img.ubq/track"
|
|
55
|
-
private const val IMAGE_FILL_URI = "fill/image/imageFileURI"
|
|
56
|
-
private const val VIDEO_FILL_URI = "fill/video/fileURI"
|
|
57
|
-
|
|
58
|
-
// ---- Timing ------------------------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
/** Seconds the montage is pushed back so it starts after the title card (0 when there's no title). */
|
|
61
|
-
private fun titleOffset(hasTitle: Boolean): Double = if (hasTitle) TITLE_DURATION - 1.0 else 0.0
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* The crossfade overlap between two consecutive clips. Two photos crossfade, so they overlap by
|
|
65
|
-
* [OVERLAP_DURATION]; anything next to a video hard-cuts (no animation), so there is no overlap —
|
|
66
|
-
* the clips play back-to-back.
|
|
67
|
-
*/
|
|
68
|
-
private fun overlapBetween(
|
|
69
|
-
a: ImageItem,
|
|
70
|
-
b: ImageItem,
|
|
71
|
-
): Double = if (a.isVideo || b.isVideo) 0.0 else OVERLAP_DURATION
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Absolute page time at which each slot appears. Each clip advances by its own on-timeline duration
|
|
75
|
-
* ([durations], one per slot — a photo is always [IMAGE_DURATION]; a short video is only as long as
|
|
76
|
-
* its source) minus the overlap with the previous clip, so photo→photo boundaries overlap for the
|
|
77
|
-
* crossfade while video-adjacent boundaries are back-to-back with no gap.
|
|
78
|
-
*/
|
|
79
|
-
internal fun slotStartTimes(
|
|
80
|
-
media: List<ImageItem>,
|
|
81
|
-
durations: List<Double>,
|
|
82
|
-
hasTitle: Boolean,
|
|
83
|
-
): List<Double> {
|
|
84
|
-
val starts = ArrayList<Double>(media.size)
|
|
85
|
-
var t = titleOffset(hasTitle)
|
|
86
|
-
for (i in media.indices) {
|
|
87
|
-
if (i > 0) t += durations.getOrElse(i - 1) { IMAGE_DURATION } - overlapBetween(media[i - 1], media[i])
|
|
88
|
-
starts.add(t)
|
|
89
|
-
}
|
|
90
|
-
return starts
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ---- Spacer (burst-image intro only) -----------------------------------------------------------
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* An empty, full-page graphic block used purely for timing (only the title's burst intro uses these
|
|
97
|
-
* now — the main slides are positioned by their track's time offset instead).
|
|
98
|
-
*/
|
|
99
|
-
internal fun createSpacerBlock(
|
|
100
|
-
engine: Engine,
|
|
101
|
-
page: Int,
|
|
102
|
-
duration: Double,
|
|
103
|
-
type: String = SPACER_BETWEEN,
|
|
104
|
-
): Int {
|
|
105
|
-
val spacer = engine.block.create(DesignBlockType.Graphic)
|
|
106
|
-
engine.block.setWidth(spacer, engine.block.getWidth(page))
|
|
107
|
-
engine.block.setHeight(spacer, engine.block.getHeight(page))
|
|
108
|
-
engine.block.setDuration(spacer, duration)
|
|
109
|
-
engine.block.setScopeEnabled(spacer, "editor/select", false)
|
|
110
|
-
engine.block.setMetadata(spacer, SPACER_TYPE_KEY, type)
|
|
111
|
-
return spacer
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ---- Block classification ----------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
/** A media slot (image or video): a graphic block carrying [IMAGE_INDEX_KEY] and no spacer tag. */
|
|
117
|
-
private fun isImageBlock(
|
|
118
|
-
engine: Engine,
|
|
119
|
-
block: Int,
|
|
120
|
-
): Boolean = try {
|
|
121
|
-
engine.block.getType(block) == GRAPHIC_TYPE &&
|
|
122
|
-
engine.block.hasMetadata(block, IMAGE_INDEX_KEY) &&
|
|
123
|
-
!engine.block.hasMetadata(block, SPACER_TYPE_KEY)
|
|
124
|
-
} catch (e: Exception) {
|
|
125
|
-
false
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** True if the block carries the video media-type tag. */
|
|
129
|
-
internal fun isVideoBlock(
|
|
130
|
-
engine: Engine,
|
|
131
|
-
block: Int,
|
|
132
|
-
): Boolean = try {
|
|
133
|
-
engine.block.hasMetadata(block, MEDIA_TYPE_KEY) &&
|
|
134
|
-
engine.block.getMetadata(block, MEDIA_TYPE_KEY) == MEDIA_TYPE_VIDEO
|
|
135
|
-
} catch (e: Exception) {
|
|
136
|
-
false
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** The single media block that lives on a media [track], or null if it has none yet. */
|
|
140
|
-
private fun mediaBlockOf(
|
|
141
|
-
engine: Engine,
|
|
142
|
-
track: Int,
|
|
143
|
-
): Int? = engine.block.getChildren(track).firstOrNull { isImageBlock(engine, it) }
|
|
144
|
-
|
|
145
|
-
/** The media clips across all media tracks, in slot order (matches z / render order). */
|
|
146
|
-
internal fun imageBlocksInOrder(
|
|
147
|
-
engine: Engine,
|
|
148
|
-
tracks: SlideshowTracks,
|
|
149
|
-
): List<Int> = tracks.mediaTracks.mapNotNull { mediaBlockOf(engine, it) }
|
|
150
|
-
|
|
151
|
-
// ---- Track location by metadata ----------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* The named tracks that make up the slideshow, located by their [TRACK_ROLE_KEY] metadata.
|
|
155
|
-
* [mediaTracks] holds one track per slide, ordered by slot index (which equals their z order);
|
|
156
|
-
* [textTrack] is null when the slideshow has no title.
|
|
157
|
-
*/
|
|
158
|
-
data class SlideshowTracks(
|
|
159
|
-
val mediaTracks: List<Int>,
|
|
160
|
-
val textTrack: Int?,
|
|
161
|
-
val backgroundTrack: Int? = null,
|
|
162
|
-
val matteTrack: Int? = null,
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Tag the persistent single-purpose tracks with their role so [findTracks] can re-locate them.
|
|
167
|
-
* Media tracks are tagged individually in [createMediaTrack].
|
|
168
|
-
*/
|
|
169
|
-
fun tagSlideshowTracks(
|
|
170
|
-
engine: Engine,
|
|
171
|
-
textTrack: DesignBlock,
|
|
172
|
-
backgroundTrack: DesignBlock,
|
|
173
|
-
matteTrack: DesignBlock,
|
|
174
|
-
) {
|
|
175
|
-
engine.block.setMetadata(textTrack, TRACK_ROLE_KEY, TRACK_ROLE_TEXT)
|
|
176
|
-
engine.block.setMetadata(backgroundTrack, TRACK_ROLE_KEY, TRACK_ROLE_BACKGROUND)
|
|
177
|
-
engine.block.setMetadata(matteTrack, TRACK_ROLE_KEY, TRACK_ROLE_MATTE)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* The persistent full-page backdrop block (the single child of the background track), or null when
|
|
182
|
-
* the background track or its block isn't present. The Styles application paints this block per
|
|
183
|
-
* style (hidden for Default, white for Noir, a looping video for Hologram/Bubblegum).
|
|
184
|
-
*/
|
|
185
|
-
fun backgroundBlock(
|
|
186
|
-
engine: Engine,
|
|
187
|
-
page: Int,
|
|
188
|
-
): Int? {
|
|
189
|
-
val track = findTracks(engine, page)?.backgroundTrack ?: return null
|
|
190
|
-
return engine.block.getChildren(track).firstOrNull()
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* The persistent black matte block (the single child of the matte track), sized to the media so a
|
|
195
|
-
* crossfade between clips fades to black instead of revealing the moving video backdrop below it.
|
|
196
|
-
* Null when the matte track or its block isn't present.
|
|
197
|
-
*/
|
|
198
|
-
fun matteBlock(
|
|
199
|
-
engine: Engine,
|
|
200
|
-
page: Int,
|
|
201
|
-
): Int? {
|
|
202
|
-
val track = findTracks(engine, page)?.matteTrack ?: return null
|
|
203
|
-
return engine.block.getChildren(track).firstOrNull()
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* The slot index tagged on a media track's clip, used to order the tracks. Missing/garbled metadata
|
|
208
|
-
* sorts last so a half-built track never jumps to the front.
|
|
209
|
-
*/
|
|
210
|
-
private fun mediaTrackIndex(
|
|
211
|
-
engine: Engine,
|
|
212
|
-
track: Int,
|
|
213
|
-
): Int {
|
|
214
|
-
val block = mediaBlockOf(engine, track) ?: return Int.MAX_VALUE
|
|
215
|
-
return runCatching { engine.block.getMetadata(block, IMAGE_INDEX_KEY).toInt() }.getOrDefault(Int.MAX_VALUE)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Locate the slideshow tracks on [page] by their [TRACK_ROLE_KEY] metadata, ignoring burst tracks
|
|
220
|
-
* (which carry no role). The media tracks are returned in slot order. Returns null if no media
|
|
221
|
-
* tracks are present yet (the page hasn't been set up); the text track is optional and absent when
|
|
222
|
-
* the slideshow has no title. Resolving the tracks fresh on each use avoids retaining block ids,
|
|
223
|
-
* which the engine can invalidate over time (e.g. on undo/redo or a scene reload).
|
|
224
|
-
*/
|
|
225
|
-
fun findTracks(
|
|
226
|
-
engine: Engine,
|
|
227
|
-
page: Int,
|
|
228
|
-
): SlideshowTracks? {
|
|
229
|
-
val mediaTracks = mutableListOf<Int>()
|
|
230
|
-
var textTrack: Int? = null
|
|
231
|
-
var backgroundTrack: Int? = null
|
|
232
|
-
var matteTrack: Int? = null
|
|
233
|
-
// Search the whole scene by type rather than walking page children: the video timeline nests
|
|
234
|
-
// tracks under intermediate blocks, so they aren't always direct children of the page.
|
|
235
|
-
for (t in engine.block.findByType(DesignBlockType.Track)) {
|
|
236
|
-
val role = try {
|
|
237
|
-
if (engine.block.hasMetadata(t, TRACK_ROLE_KEY)) engine.block.getMetadata(t, TRACK_ROLE_KEY) else null
|
|
238
|
-
} catch (e: Exception) {
|
|
239
|
-
null
|
|
240
|
-
}
|
|
241
|
-
when (role) {
|
|
242
|
-
TRACK_ROLE_MEDIA -> mediaTracks.add(t)
|
|
243
|
-
TRACK_ROLE_TEXT -> textTrack = t
|
|
244
|
-
TRACK_ROLE_BACKGROUND -> backgroundTrack = t
|
|
245
|
-
TRACK_ROLE_MATTE -> matteTrack = t
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
// Return the scene's tracks even when there is no media (e.g. after "Clear all") so media can
|
|
249
|
-
// still be re-added; only bail when the scene scaffold itself hasn't been created yet. Otherwise
|
|
250
|
-
// clearing every clip would leave findTracks returning null, and the media dock / next apply
|
|
251
|
-
// would bail early with no way back except restarting the editor.
|
|
252
|
-
if (mediaTracks.isEmpty() && textTrack == null && backgroundTrack == null && matteTrack == null) return null
|
|
253
|
-
mediaTracks.sortBy { mediaTrackIndex(engine, it) }
|
|
254
|
-
return SlideshowTracks(mediaTracks, textTrack, backgroundTrack, matteTrack)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ---- Metadata read/write -----------------------------------------------------------------------
|
|
258
|
-
|
|
259
|
-
internal fun writeImageMetadata(
|
|
260
|
-
engine: Engine,
|
|
261
|
-
block: Int,
|
|
262
|
-
image: ImageItem,
|
|
263
|
-
index: Int,
|
|
264
|
-
) {
|
|
265
|
-
engine.block.setMetadata(block, IMAGE_INDEX_KEY, "$index")
|
|
266
|
-
engine.block.setMetadata(block, IMG_ID_KEY, "${image.id}")
|
|
267
|
-
engine.block.setMetadata(block, MEDIA_TYPE_KEY, if (image.isVideo) MEDIA_TYPE_VIDEO else "image")
|
|
268
|
-
image.creationDate?.let { engine.block.setMetadata(block, CREATION_DATE_KEY, "$it") }
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
private fun readCreationDate(
|
|
272
|
-
engine: Engine,
|
|
273
|
-
block: Int,
|
|
274
|
-
): Long? = if (engine.block.hasMetadata(block, CREATION_DATE_KEY)) {
|
|
275
|
-
engine.block.getMetadata(block, CREATION_DATE_KEY).toLongOrNull()
|
|
276
|
-
} else {
|
|
277
|
-
null
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** Read the media URL from a slot's fill (image or video) plus whether it is a video. */
|
|
281
|
-
private fun readMediaUrl(
|
|
282
|
-
engine: Engine,
|
|
283
|
-
block: Int,
|
|
284
|
-
): Pair<String, Boolean>? {
|
|
285
|
-
val fill = try {
|
|
286
|
-
engine.block.getFill(block)
|
|
287
|
-
} catch (e: Exception) {
|
|
288
|
-
return null
|
|
289
|
-
}
|
|
290
|
-
runCatching { engine.block.getString(fill, IMAGE_FILL_URI) }.getOrNull()?.let { return it to false }
|
|
291
|
-
runCatching { engine.block.getString(fill, VIDEO_FILL_URI) }.getOrNull()?.let { return it to true }
|
|
292
|
-
return null
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ---- Reading the grid from the tracks (source of truth) ----------------------------------------
|
|
296
|
-
|
|
297
|
-
/** Rebuild the editable media list from the actual track blocks. */
|
|
298
|
-
fun readImagesFromTracks(
|
|
299
|
-
engine: Engine,
|
|
300
|
-
tracks: SlideshowTracks,
|
|
301
|
-
): List<ImageItem> {
|
|
302
|
-
return imageBlocksInOrder(engine, tracks).mapIndexedNotNull { position, block ->
|
|
303
|
-
val (url, isVideo) = readMediaUrl(engine, block) ?: run {
|
|
304
|
-
Log.w(TAG, "Media slot $block at position $position has no readable fill; skipping")
|
|
305
|
-
return@mapIndexedNotNull null
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
val id = if (engine.block.hasMetadata(block, IMG_ID_KEY)) {
|
|
309
|
-
engine.block.getMetadata(block, IMG_ID_KEY).toIntOrNull() ?: (position + 1)
|
|
310
|
-
} else {
|
|
311
|
-
position + 1
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
ImageItem(
|
|
315
|
-
id = id,
|
|
316
|
-
url = url,
|
|
317
|
-
isVideo = isVideo,
|
|
318
|
-
creationDate = readCreationDate(engine, block),
|
|
319
|
-
)
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Compute each clip's window on the timeline from its track's time offset. Each media track holds a
|
|
325
|
-
* single clip of [IMAGE_DURATION], so the window is simply [offset, offset + IMAGE_DURATION].
|
|
326
|
-
* Returned in slot order.
|
|
327
|
-
*/
|
|
328
|
-
fun readTimelineImages(
|
|
329
|
-
engine: Engine,
|
|
330
|
-
tracks: SlideshowTracks,
|
|
331
|
-
): List<TimelineImage> = tracks.mediaTracks.mapNotNull { track ->
|
|
332
|
-
val block = mediaBlockOf(engine, track) ?: return@mapNotNull null
|
|
333
|
-
val (url, isVideo) = readMediaUrl(engine, block) ?: return@mapNotNull null
|
|
334
|
-
val start = runCatching { engine.block.getTimeOffset(track) }.getOrDefault(0.0)
|
|
335
|
-
val duration = runCatching { engine.block.getDuration(block) }.getOrDefault(IMAGE_DURATION)
|
|
336
|
-
TimelineImage(
|
|
337
|
-
url = url,
|
|
338
|
-
startTime = start,
|
|
339
|
-
endTime = start + duration,
|
|
340
|
-
// Each clip has a symmetric in/out crossfade, so it is fully opaque and the only one on
|
|
341
|
-
// screen at its midpoint.
|
|
342
|
-
fullyVisibleTime = start + duration / 2.0,
|
|
343
|
-
isVideo = isVideo,
|
|
344
|
-
)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* If a title is present, returns the time at which the burst images have finished and the title is
|
|
349
|
-
* visible unobstructed (the latest burst-image end across the burst tracks). Returns null when
|
|
350
|
-
* there is no title.
|
|
351
|
-
*/
|
|
352
|
-
fun readTitleClearTime(
|
|
353
|
-
engine: Engine,
|
|
354
|
-
page: Int,
|
|
355
|
-
tracks: SlideshowTracks,
|
|
356
|
-
): Double? {
|
|
357
|
-
val textTrack = tracks.textTrack ?: return null
|
|
358
|
-
val hasTitle = engine.block.getChildren(textTrack).any {
|
|
359
|
-
runCatching { engine.block.getType(it) == TEXT_TYPE }.getOrDefault(false)
|
|
360
|
-
}
|
|
361
|
-
if (!hasTitle) return null
|
|
362
|
-
|
|
363
|
-
// Exclude every non-burst track: media, text, and the full-length backdrop/matte tracks. Without
|
|
364
|
-
// the last two, they are mistaken for burst tracks and their slideshow-length duration becomes
|
|
365
|
-
// maxBurstEnd, so tapping the title chip seeks to the very end instead of just past the intro.
|
|
366
|
-
val known = (tracks.mediaTracks + textTrack + listOfNotNull(tracks.backgroundTrack, tracks.matteTrack)).toSet()
|
|
367
|
-
var maxBurstEnd = 0.0
|
|
368
|
-
for (child in engine.block.getChildren(page)) {
|
|
369
|
-
if (child in known) continue
|
|
370
|
-
if (runCatching { engine.block.getType(child) }.getOrNull() != TRACK_TYPE) continue
|
|
371
|
-
// Burst track: a burst image is a graphic with no spacer tag and no slot index.
|
|
372
|
-
var t = 0.0
|
|
373
|
-
for (b in engine.block.getChildren(child)) {
|
|
374
|
-
val d = runCatching { engine.block.getDuration(b) }.getOrDefault(0.0)
|
|
375
|
-
t += d
|
|
376
|
-
val isBurstImage = runCatching {
|
|
377
|
-
engine.block.getType(b) == GRAPHIC_TYPE &&
|
|
378
|
-
!engine.block.hasMetadata(b, SPACER_TYPE_KEY) &&
|
|
379
|
-
!engine.block.hasMetadata(b, IMAGE_INDEX_KEY)
|
|
380
|
-
}.getOrDefault(false)
|
|
381
|
-
if (isBurstImage) maxBurstEnd = maxOf(maxBurstEnd, t)
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return maxBurstEnd
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// ---- Media tracks ------------------------------------------------------------------------------
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Create a full-page media track appended on top of the current stack (so it renders above earlier
|
|
391
|
-
* slides) and positioned at [startTime] via its time offset. Callers add the single media block.
|
|
392
|
-
*/
|
|
393
|
-
internal fun createMediaTrack(
|
|
394
|
-
engine: Engine,
|
|
395
|
-
page: Int,
|
|
396
|
-
startTime: Double,
|
|
397
|
-
): Int {
|
|
398
|
-
val track = engine.block.create(DesignBlockType.Track)
|
|
399
|
-
engine.block.setMetadata(track, TRACK_ROLE_KEY, TRACK_ROLE_MEDIA)
|
|
400
|
-
engine.block.appendChild(page, track)
|
|
401
|
-
engine.block.fillParent(track)
|
|
402
|
-
positionMediaTrack(engine, track, startTime)
|
|
403
|
-
return track
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/** Position a media track at [startTime] on the page timeline. */
|
|
407
|
-
internal fun positionMediaTrack(
|
|
408
|
-
engine: Engine,
|
|
409
|
-
track: Int,
|
|
410
|
-
startTime: Double,
|
|
411
|
-
) {
|
|
412
|
-
engine.block.setTimeOffset(track, startTime)
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Each media track's on-timeline clip length (the block's own duration), in slot order. A photo (and
|
|
417
|
-
* a video at least as long as the slot) is [IMAGE_DURATION]; a short video is only as long as its
|
|
418
|
-
* source (see [placeVideoBlock]). Falls back to [IMAGE_DURATION] for any track missing its clip.
|
|
419
|
-
*/
|
|
420
|
-
private fun slotDurations(
|
|
421
|
-
engine: Engine,
|
|
422
|
-
mediaTracks: List<Int>,
|
|
423
|
-
): List<Double> = mediaTracks.map { track ->
|
|
424
|
-
val block = mediaBlockOf(engine, track)
|
|
425
|
-
block?.let { runCatching { engine.block.getDuration(it) }.getOrNull() } ?: IMAGE_DURATION
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Re-position every media track on [page] by the start time its slot occupies, derived from the
|
|
430
|
-
* clips' actual durations (so a short video takes up only its own length, with no gap after it) and
|
|
431
|
-
* the video-aware overlaps. Returns the per-slot durations so the caller can size the page/backdrops.
|
|
432
|
-
* Call after all clip blocks for [media] have been created/edited so their durations are final.
|
|
433
|
-
*/
|
|
434
|
-
internal fun repositionTracks(
|
|
435
|
-
engine: Engine,
|
|
436
|
-
page: Int,
|
|
437
|
-
media: List<ImageItem>,
|
|
438
|
-
hasTitle: Boolean,
|
|
439
|
-
): List<Double> {
|
|
440
|
-
val mediaTracks = findTracks(engine, page)?.mediaTracks ?: return emptyList()
|
|
441
|
-
val durations = slotDurations(engine, mediaTracks)
|
|
442
|
-
val starts = slotStartTimes(media, durations, hasTitle)
|
|
443
|
-
mediaTracks.forEachIndexed { k, track ->
|
|
444
|
-
positionMediaTrack(engine, track, starts.getOrElse(k) { 0.0 })
|
|
445
|
-
}
|
|
446
|
-
return durations
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// ---- Animation ---------------------------------------------------------------------------------
|
|
450
|
-
|
|
451
|
-
/** True if the neighbour before slot [index] is a video (so this clip must hard-cut in). */
|
|
452
|
-
private fun hardCutIn(
|
|
453
|
-
media: List<ImageItem>,
|
|
454
|
-
index: Int,
|
|
455
|
-
): Boolean = index > 0 && media[index - 1].isVideo
|
|
456
|
-
|
|
457
|
-
/** True if the neighbour after slot [index] is a video (so this clip must hard-cut out). */
|
|
458
|
-
private fun hardCutOut(
|
|
459
|
-
media: List<ImageItem>,
|
|
460
|
-
index: Int,
|
|
461
|
-
): Boolean = index < media.size - 1 && media[index + 1].isVideo
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Apply a random slide animation to an image [block], honouring hard-cut boundaries: a photo that
|
|
465
|
-
* sits next to a video drops its fade on that side ([suppressIn]/[suppressOut]) so the photo↔video
|
|
466
|
-
* cut is clean instead of a fade fighting the video's hard edge. Suppressing a side simply leaves it
|
|
467
|
-
* unset (no in/out animation = a hard cut), so callers must only use this on a freshly-created block
|
|
468
|
-
* or one that already has no animation on the suppressed side.
|
|
469
|
-
*/
|
|
470
|
-
internal fun applySlideAnimation(
|
|
471
|
-
engine: Engine,
|
|
472
|
-
block: Int,
|
|
473
|
-
suppressIn: Boolean,
|
|
474
|
-
suppressOut: Boolean,
|
|
475
|
-
animationPairs: List<AnimationPair>,
|
|
476
|
-
) {
|
|
477
|
-
if (animationPairs.isEmpty()) return
|
|
478
|
-
val pair = Animations.getRandomAnimationPair(animationPairs)
|
|
479
|
-
|
|
480
|
-
// Engine animations are single-owner (1:1 with a design block), so each slide gets its own fresh
|
|
481
|
-
// animation blocks — never a shared/pooled one. First drop any animation this block already owns
|
|
482
|
-
// (re-applying on repaint would otherwise leak it: setIn/OutAnimation don't destroy the old one).
|
|
483
|
-
clearSlideAnimations(engine, block)
|
|
484
|
-
|
|
485
|
-
if (!suppressIn) {
|
|
486
|
-
val inAnimation = pair.createIn(engine)
|
|
487
|
-
engine.block.setInAnimation(block, inAnimation)
|
|
488
|
-
// A Ken Burns zoom-out must be pre-scaled so its pan never reveals the canvas edges; other
|
|
489
|
-
// animations (e.g. blur) have no such property, so the read returns 0 and nothing happens.
|
|
490
|
-
val zoom = runCatching {
|
|
491
|
-
engine.block.getFloat(inAnimation, "animation/ken_burns/zoomIntensity")
|
|
492
|
-
}.getOrDefault(0f)
|
|
493
|
-
if (zoom < 0f) engine.block.scale(block, 1f + abs(zoom), anchorX = 0.5f, anchorY = 0.5f)
|
|
494
|
-
}
|
|
495
|
-
if (!suppressOut) {
|
|
496
|
-
val outAnimation = pair.createOut(engine)
|
|
497
|
-
engine.block.setOutAnimation(block, outAnimation)
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Destroy the in/out animation blocks [block] currently owns. The engine's setIn/OutAnimation do not
|
|
503
|
-
* auto-destroy the animation they replace, so re-applying a slide animation without this would leak an
|
|
504
|
-
* orphaned animation block on every edit. Destroying the animation also clears the block's reference
|
|
505
|
-
* to it, leaving a clean base for [applySlideAnimation] to set fresh (or leave a suppressed side bare).
|
|
506
|
-
*/
|
|
507
|
-
private fun clearSlideAnimations(
|
|
508
|
-
engine: Engine,
|
|
509
|
-
block: Int,
|
|
510
|
-
) {
|
|
511
|
-
listOf(
|
|
512
|
-
runCatching { engine.block.getInAnimation(block) }.getOrDefault(-1),
|
|
513
|
-
runCatching { engine.block.getOutAnimation(block) }.getOrDefault(-1),
|
|
514
|
-
).forEach { animation ->
|
|
515
|
-
if (animation != -1 && runCatching { engine.block.isValid(animation) }.getOrDefault(false)) {
|
|
516
|
-
engine.block.destroy(animation)
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// ---- Media block creation ----------------------------------------------------------------------
|
|
522
|
-
|
|
523
|
-
private fun createImageBlock(
|
|
524
|
-
engine: Engine,
|
|
525
|
-
page: Int,
|
|
526
|
-
image: ImageItem,
|
|
527
|
-
index: Int,
|
|
528
|
-
media: List<ImageItem>,
|
|
529
|
-
animationPairs: List<AnimationPair>,
|
|
530
|
-
): Int {
|
|
531
|
-
val block = engine.block.create(DesignBlockType.Graphic)
|
|
532
|
-
engine.block.setScopeEnabled(block, "editor/select", false)
|
|
533
|
-
engine.block.setWidth(block, engine.block.getWidth(page))
|
|
534
|
-
engine.block.setHeight(block, engine.block.getHeight(page))
|
|
535
|
-
|
|
536
|
-
val fill = engine.block.createFill(FillType.Image)
|
|
537
|
-
val shape = engine.block.createShape(ShapeType.Rect)
|
|
538
|
-
engine.block.setFill(block, fill)
|
|
539
|
-
engine.block.setString(fill, IMAGE_FILL_URI, image.url)
|
|
540
|
-
engine.block.setShape(block, shape)
|
|
541
|
-
engine.block.setDuration(block, IMAGE_DURATION)
|
|
542
|
-
|
|
543
|
-
// Set animations before the index metadata is used elsewhere; a photo next to a video hard-cuts
|
|
544
|
-
// on that side so the fade never fights the video's hard edge.
|
|
545
|
-
applySlideAnimation(engine, block, hardCutIn(media, index), hardCutOut(media, index), animationPairs)
|
|
546
|
-
|
|
547
|
-
writeImageMetadata(engine, block, image, index)
|
|
548
|
-
return block
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Place a video slot and trim it for the [IMAGE_DURATION] window: create the [FillType.Video] block,
|
|
553
|
-
* no animation (videos hard-cut — no fade/Ken Burns/zoom), metadata, then `forceLoadAVResource`
|
|
554
|
-
* (cheap, ~100ms) and trim by source length:
|
|
555
|
-
* - shorter than the slot → play the whole source once at its natural length and shrink the block
|
|
556
|
-
* to match, so the clip is only as long as the video (no looping, no blank tail); the layout
|
|
557
|
-
* starts the next clip right after,
|
|
558
|
-
* - longer than the slot → take the middle [IMAGE_DURATION] (centered on the clip so the window
|
|
559
|
-
* starts a little after the source start and ends before its end, landing on the action),
|
|
560
|
-
* - unknown duration → trim [IMAGE_DURATION] from the start and loop.
|
|
561
|
-
*/
|
|
562
|
-
suspend fun placeVideoBlock(
|
|
563
|
-
engine: Engine,
|
|
564
|
-
page: Int,
|
|
565
|
-
image: ImageItem,
|
|
566
|
-
index: Int,
|
|
567
|
-
): Int {
|
|
568
|
-
val block = engine.block.create(DesignBlockType.Graphic)
|
|
569
|
-
engine.block.setScopeEnabled(block, "editor/select", false)
|
|
570
|
-
engine.block.setWidth(block, engine.block.getWidth(page))
|
|
571
|
-
engine.block.setHeight(block, engine.block.getHeight(page))
|
|
572
|
-
|
|
573
|
-
val fill = engine.block.createFill(FillType.Video)
|
|
574
|
-
val shape = engine.block.createShape(ShapeType.Rect)
|
|
575
|
-
engine.block.setFill(block, fill)
|
|
576
|
-
engine.block.setString(fill, VIDEO_FILL_URI, image.url)
|
|
577
|
-
engine.block.setShape(block, shape)
|
|
578
|
-
engine.block.setDuration(block, IMAGE_DURATION)
|
|
579
|
-
|
|
580
|
-
// forceLoadAVResource is cheap (~100ms); the video needs it to render and to query its duration.
|
|
581
|
-
engine.block.forceLoadAVResource(fill)
|
|
582
|
-
val sourceDuration = runCatching { engine.block.getAVResourceTotalDuration(fill) }.getOrDefault(0.0)
|
|
583
|
-
|
|
584
|
-
when {
|
|
585
|
-
sourceDuration in 0.0..IMAGE_DURATION && sourceDuration > 0.0 -> {
|
|
586
|
-
// Shorter than the slot: play the source once at its natural length and shrink the block
|
|
587
|
-
// to match, so the clip occupies only as much of the timeline as the video is long — no
|
|
588
|
-
// looping and no blank tail. The slot layout picks up this shorter block duration.
|
|
589
|
-
engine.block.setTrimOffset(fill, 0.0)
|
|
590
|
-
engine.block.setTrimLength(fill, sourceDuration)
|
|
591
|
-
engine.block.setLooping(fill, false)
|
|
592
|
-
engine.block.setDuration(block, sourceDuration)
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
sourceDuration > IMAGE_DURATION -> {
|
|
596
|
-
// Longer than the slot: take the middle IMAGE_DURATION so the window is centered on the
|
|
597
|
-
// clip (starts after the source start, ends before its end) rather than the lead-in.
|
|
598
|
-
engine.block.setTrimOffset(fill, (sourceDuration - IMAGE_DURATION) / 2.0)
|
|
599
|
-
engine.block.setTrimLength(fill, IMAGE_DURATION)
|
|
600
|
-
engine.block.setLooping(fill, false)
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
else -> {
|
|
604
|
-
// Unknown duration: trim IMAGE_DURATION from the start and loop to be safe.
|
|
605
|
-
engine.block.setTrimOffset(fill, 0.0)
|
|
606
|
-
engine.block.setTrimLength(fill, IMAGE_DURATION)
|
|
607
|
-
engine.block.setLooping(fill, true)
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// No animation on videos — they hard-cut in/out.
|
|
612
|
-
writeImageMetadata(engine, block, image, index)
|
|
613
|
-
return block
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
/** Create the media block for slot [index] on the right kind of block (image vs video). */
|
|
617
|
-
internal suspend fun createMediaBlock(
|
|
618
|
-
engine: Engine,
|
|
619
|
-
page: Int,
|
|
620
|
-
index: Int,
|
|
621
|
-
media: List<ImageItem>,
|
|
622
|
-
animationPairs: List<AnimationPair>,
|
|
623
|
-
): Int {
|
|
624
|
-
val image = media[index]
|
|
625
|
-
return if (image.isVideo) {
|
|
626
|
-
placeVideoBlock(engine, page, image, index)
|
|
627
|
-
} else {
|
|
628
|
-
createImageBlock(engine, page, image, index, media, animationPairs)
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// ---- Total duration ----------------------------------------------------------------------------
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Total page duration for [media] laid out by [slotStartTimes] with the given per-slot [durations]
|
|
636
|
-
* (overlaps vary with video adjacency). The last clip ends its own duration after its start; a title
|
|
637
|
-
* keeps a 1s tail after the last clip.
|
|
638
|
-
*/
|
|
639
|
-
fun calculateTotalDuration(
|
|
640
|
-
media: List<ImageItem>,
|
|
641
|
-
durations: List<Double>,
|
|
642
|
-
hasTitle: Boolean,
|
|
643
|
-
): Double {
|
|
644
|
-
if (media.isEmpty() || durations.isEmpty()) return if (hasTitle) TITLE_DURATION else IMAGE_DURATION
|
|
645
|
-
val lastStart = slotStartTimes(media, durations, hasTitle).last()
|
|
646
|
-
return lastStart + durations.last() + if (hasTitle) 1.0 else 0.0
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// ---- Applying a staged edit in-place -----------------------------------------------------------
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Write [staged] back into the tracks in-place:
|
|
653
|
-
* - reuse an existing media track when both it and the staged item are the same media kind — an
|
|
654
|
-
* image slot is repainted (preserving per-block effects/filters) + re-animated; otherwise the
|
|
655
|
-
* slot's clip is rebuilt in the **same** track (keeping its z position) — e.g. image↔video, any
|
|
656
|
-
* video (trim can't just be repainted), or a photo that must gain/lose a hard-cut edge,
|
|
657
|
-
* - create a new track for each added item (appended on top → highest slot index → correct z),
|
|
658
|
-
* - destroy surplus tracks for deleted items,
|
|
659
|
-
* - then re-position every track by its slot's start time and recompute the page duration.
|
|
660
|
-
*
|
|
661
|
-
* Video clips are placed + loaded + trimmed here (forceLoad is cheap, so they render right away);
|
|
662
|
-
* suspends for the cheap per-video `forceLoadAVResource`.
|
|
663
|
-
*/
|
|
664
|
-
suspend fun applyDraftToTracks(
|
|
665
|
-
engine: Engine,
|
|
666
|
-
page: Int,
|
|
667
|
-
tracks: SlideshowTracks,
|
|
668
|
-
staged: List<ImageItem>,
|
|
669
|
-
animationPairs: List<AnimationPair>,
|
|
670
|
-
hasTitle: Boolean,
|
|
671
|
-
) {
|
|
672
|
-
val existing = tracks.mediaTracks
|
|
673
|
-
val m = existing.size
|
|
674
|
-
val n = staged.size
|
|
675
|
-
val common = min(m, n)
|
|
676
|
-
|
|
677
|
-
// Reuse or rebuild the clip on each retained track.
|
|
678
|
-
for (k in 0 until common) {
|
|
679
|
-
val track = existing[k]
|
|
680
|
-
val block = mediaBlockOf(engine, track)
|
|
681
|
-
val stagedIsVideo = staged[k].isVideo
|
|
682
|
-
val canRepaint = block != null &&
|
|
683
|
-
!stagedIsVideo &&
|
|
684
|
-
!isVideoBlock(engine, block) &&
|
|
685
|
-
!needsAnimationRebuild(engine, block, staged, k)
|
|
686
|
-
if (canRepaint && block != null) {
|
|
687
|
-
// image -> image: repaint in place (keeps effects/filters), re-pick the slide animation.
|
|
688
|
-
// The `block != null` above is redundant with canRepaint but lets the compiler smart-cast
|
|
689
|
-
// block to non-null inside this branch.
|
|
690
|
-
val fill = engine.block.getFill(block)
|
|
691
|
-
engine.block.setString(fill, IMAGE_FILL_URI, staged[k].url)
|
|
692
|
-
applySlideAnimation(engine, block, hardCutIn(staged, k), hardCutOut(staged, k), animationPairs)
|
|
693
|
-
writeImageMetadata(engine, block, staged[k], k)
|
|
694
|
-
} else {
|
|
695
|
-
// Rebuild the clip inside the same track so its z position (slot order) is preserved.
|
|
696
|
-
block?.let { engine.block.destroy(it) }
|
|
697
|
-
engine.block.appendChild(track, createMediaBlock(engine, page, k, staged, animationPairs))
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// Append a new track per addition (k is the largest index so appending keeps z == slot order).
|
|
702
|
-
// Positioned at 0 for now; every track is repositioned below once all clip durations are final.
|
|
703
|
-
for (k in common until n) {
|
|
704
|
-
val track = createMediaTrack(engine, page, 0.0)
|
|
705
|
-
engine.block.appendChild(track, createMediaBlock(engine, page, k, staged, animationPairs))
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// Destroy surplus tracks (and their clips) for deletions.
|
|
709
|
-
for (k in n until m) {
|
|
710
|
-
engine.block.destroy(existing[k])
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// Re-locate the surviving tracks fresh (now re-indexed 0..n-1) and re-position each by its slot,
|
|
714
|
-
// using the clips' final durations (a short video only takes up its own length). Recomputing from
|
|
715
|
-
// `staged` means a reorder that moves a clip next to (or away from) a video gets both the right
|
|
716
|
-
// overlap and the right animation (via the reuse/rebuild above).
|
|
717
|
-
val durations = repositionTracks(engine, page, staged, hasTitle)
|
|
718
|
-
engine.block.setDuration(page, calculateTotalDuration(staged, durations, hasTitle))
|
|
719
|
-
|
|
720
|
-
// Checkpoint the post-edit state so this whole batch of track edits is undoable as one step.
|
|
721
|
-
// Safe now that features/removeForegroundTracksOnSceneLoad is forced false in onCreate — without
|
|
722
|
-
// that, restoring this snapshot would flatten the multi-track slideshow.
|
|
723
|
-
engine.editor.addUndoStep()
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* True if a repaint can't correctly set slot [k]'s animation because the clip currently carries an
|
|
728
|
-
* animation on a side that must now hard-cut (photo became adjacent to a video). Rebuilding the clip
|
|
729
|
-
* gives it a clean, animation-free base to which [applySlideAnimation] adds only the wanted sides.
|
|
730
|
-
*/
|
|
731
|
-
private fun needsAnimationRebuild(
|
|
732
|
-
engine: Engine,
|
|
733
|
-
block: Int,
|
|
734
|
-
staged: List<ImageItem>,
|
|
735
|
-
k: Int,
|
|
736
|
-
): Boolean {
|
|
737
|
-
val hasIn = runCatching { engine.block.getInAnimation(block) }.getOrDefault(-1) != -1
|
|
738
|
-
val hasOut = runCatching { engine.block.getOutAnimation(block) }.getOrDefault(-1) != -1
|
|
739
|
-
return (hardCutIn(staged, k) && hasIn) || (hardCutOut(staged, k) && hasOut)
|
|
740
|
-
}
|