@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
package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/FilterStylesSheet.kt
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
package ly.img.editor.configuration.memories.sheet
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.background
|
|
4
|
+
import androidx.compose.foundation.border
|
|
5
|
+
import androidx.compose.foundation.clickable
|
|
6
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
7
|
+
import androidx.compose.foundation.layout.Box
|
|
8
|
+
import androidx.compose.foundation.layout.Column
|
|
9
|
+
import androidx.compose.foundation.layout.PaddingValues
|
|
10
|
+
import androidx.compose.foundation.layout.Spacer
|
|
11
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
12
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
13
|
+
import androidx.compose.foundation.layout.height
|
|
14
|
+
import androidx.compose.foundation.layout.padding
|
|
15
|
+
import androidx.compose.foundation.layout.size
|
|
16
|
+
import androidx.compose.foundation.lazy.LazyRow
|
|
17
|
+
import androidx.compose.foundation.lazy.items
|
|
18
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
19
|
+
import androidx.compose.material3.Icon
|
|
20
|
+
import androidx.compose.material3.MaterialTheme
|
|
21
|
+
import androidx.compose.material3.Text
|
|
22
|
+
import androidx.compose.runtime.Composable
|
|
23
|
+
import androidx.compose.runtime.getValue
|
|
24
|
+
import androidx.compose.runtime.mutableStateOf
|
|
25
|
+
import androidx.compose.runtime.remember
|
|
26
|
+
import androidx.compose.runtime.setValue
|
|
27
|
+
import androidx.compose.ui.Alignment
|
|
28
|
+
import androidx.compose.ui.Modifier
|
|
29
|
+
import androidx.compose.ui.draw.clip
|
|
30
|
+
import androidx.compose.ui.graphics.Color
|
|
31
|
+
import androidx.compose.ui.layout.ContentScale
|
|
32
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
33
|
+
import androidx.compose.ui.text.style.TextAlign
|
|
34
|
+
import androidx.compose.ui.unit.dp
|
|
35
|
+
import coil.compose.AsyncImage
|
|
36
|
+
import ly.img.editor.configuration.memories.iconPack.Image
|
|
37
|
+
import ly.img.editor.configuration.memories.style.VideoStyle
|
|
38
|
+
import ly.img.editor.configuration.memories.style.VideoStyles
|
|
39
|
+
import ly.img.editor.core.iconpack.IconPack
|
|
40
|
+
|
|
41
|
+
@Composable
|
|
42
|
+
fun FilterStylesSheet(
|
|
43
|
+
onFilterSelected: (VideoStyle) -> Unit,
|
|
44
|
+
thumbnails: Map<String, String>,
|
|
45
|
+
initialSelectedId: String = VideoStyles.DEFAULT.id,
|
|
46
|
+
modifier: Modifier = Modifier,
|
|
47
|
+
) {
|
|
48
|
+
// Seed from the currently-applied style so reopening the sheet keeps it highlighted.
|
|
49
|
+
var selectedStyleId by remember(initialSelectedId) { mutableStateOf(initialSelectedId) }
|
|
50
|
+
|
|
51
|
+
Column(
|
|
52
|
+
modifier = modifier
|
|
53
|
+
.fillMaxWidth()
|
|
54
|
+
.padding(16.dp),
|
|
55
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
56
|
+
) {
|
|
57
|
+
// Title
|
|
58
|
+
Text(
|
|
59
|
+
text = "Styles",
|
|
60
|
+
style = MaterialTheme.typography.headlineSmall,
|
|
61
|
+
fontWeight = FontWeight.Medium,
|
|
62
|
+
color = MaterialTheme.colorScheme.onSurface,
|
|
63
|
+
modifier = Modifier.padding(bottom = 24.dp),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// Filter styles row — centered when it fits, horizontally scrollable on smaller screens.
|
|
67
|
+
LazyRow(
|
|
68
|
+
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
|
|
69
|
+
contentPadding = PaddingValues(horizontal = 16.dp),
|
|
70
|
+
modifier = Modifier.fillMaxWidth(),
|
|
71
|
+
) {
|
|
72
|
+
items(VideoStyles.ALL) { style ->
|
|
73
|
+
FilterStyleItem(
|
|
74
|
+
style = style,
|
|
75
|
+
thumbnailUri = thumbnails[style.id],
|
|
76
|
+
isSelected = style.id == selectedStyleId,
|
|
77
|
+
onClick = {
|
|
78
|
+
selectedStyleId = style.id
|
|
79
|
+
onFilterSelected(style)
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Spacer(modifier = Modifier.height(24.dp))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Composable
|
|
90
|
+
private fun FilterStyleItem(
|
|
91
|
+
style: VideoStyle,
|
|
92
|
+
thumbnailUri: String?,
|
|
93
|
+
isSelected: Boolean,
|
|
94
|
+
onClick: () -> Unit,
|
|
95
|
+
modifier: Modifier = Modifier,
|
|
96
|
+
) {
|
|
97
|
+
Column(
|
|
98
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
99
|
+
modifier = modifier.clickable { onClick() },
|
|
100
|
+
) {
|
|
101
|
+
// Filter preview tile
|
|
102
|
+
Box(
|
|
103
|
+
modifier = Modifier
|
|
104
|
+
.size(72.dp)
|
|
105
|
+
.clip(RoundedCornerShape(16.dp))
|
|
106
|
+
.background(
|
|
107
|
+
if (isSelected) {
|
|
108
|
+
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
|
109
|
+
} else {
|
|
110
|
+
Color.Transparent
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
.border(
|
|
114
|
+
width = if (isSelected) 3.dp else 0.dp,
|
|
115
|
+
color = if (isSelected) {
|
|
116
|
+
MaterialTheme.colorScheme.primary
|
|
117
|
+
} else {
|
|
118
|
+
Color.Transparent
|
|
119
|
+
},
|
|
120
|
+
shape = RoundedCornerShape(16.dp),
|
|
121
|
+
)
|
|
122
|
+
.padding(if (isSelected) 4.dp else 0.dp),
|
|
123
|
+
contentAlignment = Alignment.Center,
|
|
124
|
+
) {
|
|
125
|
+
Box(
|
|
126
|
+
modifier = Modifier
|
|
127
|
+
.fillMaxSize()
|
|
128
|
+
.clip(RoundedCornerShape(12.dp))
|
|
129
|
+
.background(Color(style.previewBackground)),
|
|
130
|
+
contentAlignment = Alignment.Center,
|
|
131
|
+
) {
|
|
132
|
+
if (thumbnailUri != null) {
|
|
133
|
+
// Thumbnail supplied by the custom style asset source, loaded with Coil.
|
|
134
|
+
AsyncImage(
|
|
135
|
+
model = thumbnailUri,
|
|
136
|
+
contentDescription = style.displayName,
|
|
137
|
+
contentScale = ContentScale.Crop,
|
|
138
|
+
modifier = Modifier.fillMaxSize(),
|
|
139
|
+
)
|
|
140
|
+
} else {
|
|
141
|
+
// Default style: an icon on the tile color (no filter / original look).
|
|
142
|
+
Icon(
|
|
143
|
+
imageVector = IconPack.Image,
|
|
144
|
+
contentDescription = style.displayName,
|
|
145
|
+
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
146
|
+
modifier = Modifier.size(32.dp),
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Spacer(modifier = Modifier.height(8.dp))
|
|
153
|
+
|
|
154
|
+
// Style name
|
|
155
|
+
Text(
|
|
156
|
+
text = style.displayName,
|
|
157
|
+
style = MaterialTheme.typography.bodyMedium,
|
|
158
|
+
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
|
|
159
|
+
color = if (isSelected) {
|
|
160
|
+
MaterialTheme.colorScheme.primary
|
|
161
|
+
} else {
|
|
162
|
+
MaterialTheme.colorScheme.onSurfaceVariant
|
|
163
|
+
},
|
|
164
|
+
textAlign = TextAlign.Center,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
}
|
package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/FilterStylesSheetType.kt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
package ly.img.editor.configuration.memories.sheet
|
|
2
|
+
|
|
3
|
+
import ly.img.editor.configuration.memories.style.VideoStyle
|
|
4
|
+
import ly.img.editor.configuration.memories.style.VideoStyles
|
|
5
|
+
import ly.img.editor.core.sheet.SheetStyle
|
|
6
|
+
import ly.img.editor.core.sheet.SheetType
|
|
7
|
+
|
|
8
|
+
fun createFilterStylesSheetType(
|
|
9
|
+
thumbnails: Map<String, String>,
|
|
10
|
+
initialSelectedId: String = VideoStyles.DEFAULT.id,
|
|
11
|
+
onFilterSelected: (VideoStyle) -> Unit,
|
|
12
|
+
): SheetType.Custom = SheetType.Custom(
|
|
13
|
+
style = SheetStyle(),
|
|
14
|
+
content = {
|
|
15
|
+
FilterStylesSheet(
|
|
16
|
+
onFilterSelected = onFilterSelected,
|
|
17
|
+
thumbnails = thumbnails,
|
|
18
|
+
initialSelectedId = initialSelectedId,
|
|
19
|
+
)
|
|
20
|
+
},
|
|
21
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package ly.img.editor.configuration.memories.sheet
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
4
|
+
import androidx.compose.foundation.layout.Column
|
|
5
|
+
import androidx.compose.foundation.layout.Row
|
|
6
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
7
|
+
import androidx.compose.foundation.layout.padding
|
|
8
|
+
import androidx.compose.foundation.layout.size
|
|
9
|
+
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
10
|
+
import androidx.compose.material3.Icon
|
|
11
|
+
import androidx.compose.material3.MaterialTheme
|
|
12
|
+
import androidx.compose.material3.Slider
|
|
13
|
+
import androidx.compose.material3.Text
|
|
14
|
+
import androidx.compose.runtime.Composable
|
|
15
|
+
import androidx.compose.runtime.getValue
|
|
16
|
+
import androidx.compose.runtime.mutableFloatStateOf
|
|
17
|
+
import androidx.compose.runtime.remember
|
|
18
|
+
import androidx.compose.runtime.setValue
|
|
19
|
+
import androidx.compose.ui.Alignment
|
|
20
|
+
import androidx.compose.ui.Modifier
|
|
21
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
22
|
+
import androidx.compose.ui.unit.dp
|
|
23
|
+
import ly.img.editor.configuration.memories.iconPack.VolumeDown
|
|
24
|
+
import ly.img.editor.configuration.memories.iconPack.VolumeUp
|
|
25
|
+
import ly.img.editor.core.iconpack.IconPack
|
|
26
|
+
import ly.img.editor.core.sheet.SheetStyle
|
|
27
|
+
import ly.img.editor.core.sheet.SheetType
|
|
28
|
+
|
|
29
|
+
@OptIn(ExperimentalMaterial3Api::class)
|
|
30
|
+
@Composable
|
|
31
|
+
fun VolumeSheet(
|
|
32
|
+
volume: Float,
|
|
33
|
+
onVolumeChange: (Float) -> Unit,
|
|
34
|
+
modifier: Modifier = Modifier,
|
|
35
|
+
) {
|
|
36
|
+
Column(
|
|
37
|
+
modifier = modifier
|
|
38
|
+
.fillMaxWidth()
|
|
39
|
+
.padding(24.dp),
|
|
40
|
+
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
41
|
+
) {
|
|
42
|
+
// Title
|
|
43
|
+
Text(
|
|
44
|
+
text = "Volume",
|
|
45
|
+
style = MaterialTheme.typography.headlineSmall,
|
|
46
|
+
fontWeight = FontWeight.Medium,
|
|
47
|
+
color = MaterialTheme.colorScheme.onSurface,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// Volume control
|
|
51
|
+
Row(
|
|
52
|
+
modifier = Modifier.fillMaxWidth(),
|
|
53
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
54
|
+
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
55
|
+
) {
|
|
56
|
+
Icon(
|
|
57
|
+
imageVector = IconPack.VolumeDown,
|
|
58
|
+
contentDescription = "Volume Down",
|
|
59
|
+
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
60
|
+
modifier = Modifier.size(24.dp),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
Slider(
|
|
64
|
+
value = volume,
|
|
65
|
+
onValueChange = onVolumeChange,
|
|
66
|
+
valueRange = 0f..1f,
|
|
67
|
+
modifier = Modifier.weight(1f),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
Icon(
|
|
71
|
+
imageVector = IconPack.VolumeUp,
|
|
72
|
+
contentDescription = "Volume Up",
|
|
73
|
+
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
74
|
+
modifier = Modifier.size(24.dp),
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Volume percentage
|
|
79
|
+
Text(
|
|
80
|
+
text = "${(volume * 100).toInt()}%",
|
|
81
|
+
style = MaterialTheme.typography.bodyMedium,
|
|
82
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
83
|
+
modifier = Modifier.align(Alignment.CenterHorizontally),
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fun createVolumeSheetType(
|
|
89
|
+
initialVolume: Float,
|
|
90
|
+
onVolumeChange: (Float) -> Unit,
|
|
91
|
+
): SheetType.Custom = SheetType.Custom(
|
|
92
|
+
style = SheetStyle(),
|
|
93
|
+
content = {
|
|
94
|
+
var volume by remember { mutableFloatStateOf(initialVolume) }
|
|
95
|
+
|
|
96
|
+
VolumeSheet(
|
|
97
|
+
volume = volume,
|
|
98
|
+
onVolumeChange = { newVolume ->
|
|
99
|
+
volume = newVolume
|
|
100
|
+
onVolumeChange(newVolume)
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
},
|
|
104
|
+
)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
package ly.img.editor.configuration.memories.style
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import ly.img.editor.configuration.memories.scene.backgroundBlock
|
|
5
|
+
import ly.img.editor.configuration.memories.scene.findTracks
|
|
6
|
+
import ly.img.editor.configuration.memories.scene.imageBlocksInOrder
|
|
7
|
+
import ly.img.editor.configuration.memories.scene.matteBlock
|
|
8
|
+
import ly.img.engine.Color
|
|
9
|
+
import ly.img.engine.EffectType
|
|
10
|
+
import ly.img.engine.Engine
|
|
11
|
+
import ly.img.engine.FillType
|
|
12
|
+
import ly.img.engine.FindAssetsQuery
|
|
13
|
+
import ly.img.engine.Font
|
|
14
|
+
import ly.img.engine.Typeface
|
|
15
|
+
import kotlin.math.abs
|
|
16
|
+
|
|
17
|
+
private const val TAG = "MemoriesStyles"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generic, data-driven application of a [VideoStyle] to the engine. These functions read
|
|
21
|
+
* everything they need from the style, so they never have to change when styles are added,
|
|
22
|
+
* removed, or tuned in [VideoStyles].
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Apply [style] to the whole slideshow: filter + scale every media clip, paint the backdrop and
|
|
27
|
+
* matte, restyle the title, and checkpoint it as one undo step. The tracks are the source of truth,
|
|
28
|
+
* so this reads the clips back from them rather than holding block ids.
|
|
29
|
+
*/
|
|
30
|
+
suspend fun applyStyleToSlideshow(
|
|
31
|
+
engine: Engine,
|
|
32
|
+
page: Int,
|
|
33
|
+
style: VideoStyle,
|
|
34
|
+
) {
|
|
35
|
+
val tracks = findTracks(engine, page) ?: return
|
|
36
|
+
val duration = engine.block.getDuration(page)
|
|
37
|
+
|
|
38
|
+
imageBlocksInOrder(engine, tracks).forEach { block ->
|
|
39
|
+
if (engine.block.supportsEffects(block)) {
|
|
40
|
+
engine.block.getEffects(block).indices.reversed().forEach { engine.block.removeEffect(block, it) }
|
|
41
|
+
applyStyleEffects(engine, block, style)
|
|
42
|
+
}
|
|
43
|
+
applyMediaScale(engine, block, page, style)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
backgroundBlock(engine, page)?.let { applyStyleBackground(engine, it, style, duration) }
|
|
47
|
+
matteBlock(engine, page)?.let {
|
|
48
|
+
applyMediaScale(engine, it, page, style)
|
|
49
|
+
engine.block.setDuration(it, duration)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
tracks.textTrack?.let { textTrack ->
|
|
53
|
+
engine.block.getChildren(textTrack)
|
|
54
|
+
.filter { runCatching { engine.block.getMetadata(it, "blockType") }.getOrNull() == "title" }
|
|
55
|
+
.forEach { updateTitleTypeface(engine, it, style) }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
engine.editor.addUndoStep()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Set the title's font + color for [style] (Noir reads black on its white backdrop; others white). */
|
|
62
|
+
private suspend fun updateTitleTypeface(
|
|
63
|
+
engine: Engine,
|
|
64
|
+
textBlock: Int,
|
|
65
|
+
style: VideoStyle,
|
|
66
|
+
) {
|
|
67
|
+
try {
|
|
68
|
+
engine.block.setTextColor(textBlock, Color.fromHex(style.titleTextColorHex))
|
|
69
|
+
val assets = engine.asset.findAssets(
|
|
70
|
+
sourceId = "ly.img.typeface",
|
|
71
|
+
query = FindAssetsQuery(query = style.typeface, page = 0, perPage = 100),
|
|
72
|
+
)
|
|
73
|
+
val typeface = assets.assets.firstOrNull()?.payload?.typeface ?: return
|
|
74
|
+
val font = resolveFont(typeface, style) ?: return
|
|
75
|
+
engine.block.setFont(textBlock, font.uri, typeface)
|
|
76
|
+
} catch (e: Exception) {
|
|
77
|
+
Log.w(TAG, "Could not restyle title for '${style.id}'", e)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private const val VIDEO_FILL_URI = "fill/video/fileURI"
|
|
82
|
+
|
|
83
|
+
/** Applies a style's image-filter adjustments to a single media [block]. */
|
|
84
|
+
fun applyStyleEffects(
|
|
85
|
+
engine: Engine,
|
|
86
|
+
block: Int,
|
|
87
|
+
style: VideoStyle,
|
|
88
|
+
) {
|
|
89
|
+
if (style.adjustments.isNotEmpty()) {
|
|
90
|
+
val adjustments = engine.block.createEffect(EffectType.Adjustments)
|
|
91
|
+
engine.block.appendEffect(block, adjustments)
|
|
92
|
+
style.adjustments.forEach { (property, value) ->
|
|
93
|
+
engine.block.setFloat(adjustments, property, value)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Size + center a media [block] within the [page] for the style's [VideoStyle.mediaScale]:
|
|
100
|
+
* full-bleed at 1.0, or scaled down and centered so the [VideoStyle.background] shows around it.
|
|
101
|
+
*
|
|
102
|
+
* This sets the block's **size and position** directly (rather than an `engine.block.scale` transform)
|
|
103
|
+
* so it cooperates with the slideshow's Ken Burns pan/zoom. The Ken Burns code pre-scales zoom-out
|
|
104
|
+
* clips (via `scale`, which mutates width/height) so the pan never reveals their edges; resetting the
|
|
105
|
+
* size here would wipe that out, so after re-establishing the clean centered base frame we re-apply
|
|
106
|
+
* the very same compensation on top. The base is reset first, so re-applying a style is idempotent
|
|
107
|
+
* and never compounds the scale. Blocks without an in-animation (videos, the matte) just get sized.
|
|
108
|
+
*/
|
|
109
|
+
fun applyMediaScale(
|
|
110
|
+
engine: Engine,
|
|
111
|
+
block: Int,
|
|
112
|
+
page: Int,
|
|
113
|
+
style: VideoStyle,
|
|
114
|
+
) {
|
|
115
|
+
val pageWidth = engine.block.getWidth(page)
|
|
116
|
+
val pageHeight = engine.block.getHeight(page)
|
|
117
|
+
val scale = style.mediaScale.coerceIn(0.1f, 1f)
|
|
118
|
+
|
|
119
|
+
// Clean, centered base frame at the target size.
|
|
120
|
+
engine.block.setWidth(block, pageWidth * scale)
|
|
121
|
+
engine.block.setHeight(block, pageHeight * scale)
|
|
122
|
+
engine.block.setPositionX(block, pageWidth * (1f - scale) / 2f)
|
|
123
|
+
engine.block.setPositionY(block, pageHeight * (1f - scale) / 2f)
|
|
124
|
+
|
|
125
|
+
// Re-coordinate Ken Burns: a zoom-out clip is pre-scaled around its center so its pan stays
|
|
126
|
+
// framed within the new size instead of revealing the backdrop at its edges.
|
|
127
|
+
val inAnimation = runCatching { engine.block.getInAnimation(block) }.getOrDefault(-1)
|
|
128
|
+
if (inAnimation != -1) {
|
|
129
|
+
val zoomIntensity = runCatching {
|
|
130
|
+
engine.block.getFloat(inAnimation, "animation/ken_burns/zoomIntensity")
|
|
131
|
+
}.getOrDefault(0f)
|
|
132
|
+
if (zoomIntensity < 0f) {
|
|
133
|
+
engine.block.scale(block, 1f + abs(zoomIntensity), anchorX = 0.5f, anchorY = 0.5f)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Paint the persistent [backgroundBlock] for the style's [VideoStyle.background]: hidden for
|
|
140
|
+
* [StyleBackground.None], a flat color for [StyleBackground.Solid], or a muted, looping video
|
|
141
|
+
* spanning [pageDuration] for [StyleBackground.Video].
|
|
142
|
+
*/
|
|
143
|
+
suspend fun applyStyleBackground(
|
|
144
|
+
engine: Engine,
|
|
145
|
+
backgroundBlock: Int,
|
|
146
|
+
style: VideoStyle,
|
|
147
|
+
pageDuration: Double,
|
|
148
|
+
) {
|
|
149
|
+
when (val background = style.background) {
|
|
150
|
+
StyleBackground.None -> {
|
|
151
|
+
engine.block.setVisible(backgroundBlock, false)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
is StyleBackground.Solid -> {
|
|
155
|
+
val fill = ensureFill(engine, backgroundBlock, FillType.Color)
|
|
156
|
+
engine.block.setColor(fill, "fill/color/value", Color.fromHex(background.colorHex))
|
|
157
|
+
engine.block.setVisible(backgroundBlock, true)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
is StyleBackground.Video -> {
|
|
161
|
+
// Resolve the backdrop clip from the custom local asset source rather than a hard-coded
|
|
162
|
+
// path; if the asset is missing, hide the backdrop instead of setting an empty fill.
|
|
163
|
+
val uri = engine.styleBackgroundVideoUri(background.assetId) ?: run {
|
|
164
|
+
engine.block.setVisible(backgroundBlock, false)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
val fill = ensureFill(engine, backgroundBlock, FillType.Video)
|
|
168
|
+
// Only (re)set the source + reload when it actually changed; re-applying a style (e.g.
|
|
169
|
+
// after a track rebuild) shouldn't pay the decode cost again for the same backdrop.
|
|
170
|
+
val current = runCatching { engine.block.getString(fill, VIDEO_FILL_URI) }.getOrNull()
|
|
171
|
+
if (current != uri) {
|
|
172
|
+
engine.block.setString(fill, VIDEO_FILL_URI, uri)
|
|
173
|
+
engine.block.forceLoadAVResource(fill)
|
|
174
|
+
}
|
|
175
|
+
val sourceDuration = runCatching { engine.block.getAVResourceTotalDuration(fill) }.getOrDefault(0.0)
|
|
176
|
+
engine.block.setLooping(fill, true)
|
|
177
|
+
engine.block.setMuted(fill, true)
|
|
178
|
+
engine.block.setTrimOffset(fill, 0.0)
|
|
179
|
+
if (sourceDuration > 0.0) engine.block.setTrimLength(fill, sourceDuration)
|
|
180
|
+
// Keep the backdrop the full length of the slideshow so the loop always covers it.
|
|
181
|
+
engine.block.setDuration(backgroundBlock, pageDuration)
|
|
182
|
+
engine.block.setVisible(backgroundBlock, true)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Return the [block]'s fill, reusing it when it is already the requested [type], otherwise creating
|
|
189
|
+
* a fresh fill of that type (and destroying the previous one). Lets us flip the persistent
|
|
190
|
+
* background block between a color and a video without leaking the discarded fill.
|
|
191
|
+
*/
|
|
192
|
+
private fun ensureFill(
|
|
193
|
+
engine: Engine,
|
|
194
|
+
block: Int,
|
|
195
|
+
type: FillType,
|
|
196
|
+
): Int {
|
|
197
|
+
val existing = runCatching { engine.block.getFill(block) }.getOrDefault(-1)
|
|
198
|
+
val existingValid = existing != -1 && runCatching { engine.block.isValid(existing) }.getOrDefault(false)
|
|
199
|
+
if (existingValid && runCatching { engine.block.getType(existing) }.getOrNull() == type.key) {
|
|
200
|
+
return existing
|
|
201
|
+
}
|
|
202
|
+
val fill = engine.block.createFill(type)
|
|
203
|
+
engine.block.setFill(block, fill)
|
|
204
|
+
if (existingValid) runCatching { engine.block.destroy(existing) }
|
|
205
|
+
return fill
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Picks the best font for [style] from [typeface]: the first available sub-family in the
|
|
210
|
+
* style's preference order, falling back to the typeface's first font.
|
|
211
|
+
*/
|
|
212
|
+
fun resolveFont(
|
|
213
|
+
typeface: Typeface,
|
|
214
|
+
style: VideoStyle,
|
|
215
|
+
): Font? {
|
|
216
|
+
for (weight in style.fontWeights) {
|
|
217
|
+
typeface.fonts.firstOrNull { it.subFamily == weight }?.let { return it }
|
|
218
|
+
}
|
|
219
|
+
return typeface.fonts.firstOrNull()
|
|
220
|
+
}
|
package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/StyleAssetSource.kt
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package ly.img.editor.configuration.memories.style
|
|
2
|
+
|
|
3
|
+
import androidx.core.net.toUri
|
|
4
|
+
import ly.img.engine.Engine
|
|
5
|
+
import ly.img.engine.FindAssetsQuery
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The custom **local** asset source that supplies the bundled style assets — the looping backdrop
|
|
9
|
+
* videos and every style's picker thumbnail. The files stay local (in `src/main/assets`) and are
|
|
10
|
+
* described by `assets/ly.img.memories.style/content.json`; the engine loads them through
|
|
11
|
+
* [addLocalSourceFromJSON][ly.img.engine.AssetApi.addLocalSourceFromJSON], exactly like the default
|
|
12
|
+
* IMG.LY sources. Keeping them behind an asset source means the style catalog references assets by
|
|
13
|
+
* id ([VideoStyle]) instead of hard-coding `file:///android_asset/...` paths across the kit.
|
|
14
|
+
*/
|
|
15
|
+
const val STYLE_SOURCE_ID = "ly.img.memories.style"
|
|
16
|
+
|
|
17
|
+
/** The bundled `content.json` describing [STYLE_SOURCE_ID], resolved as an `android_asset` URI. */
|
|
18
|
+
private const val STYLE_SOURCE_CONTENT_URI = "file:///android_asset/$STYLE_SOURCE_ID/content.json"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register [STYLE_SOURCE_ID] from its bundled `content.json` (idempotent). Call once while loading
|
|
22
|
+
* the other asset sources, before any style is applied or the Styles picker is shown.
|
|
23
|
+
*/
|
|
24
|
+
suspend fun Engine.registerStyleAssetSource() {
|
|
25
|
+
if (STYLE_SOURCE_ID !in asset.findAllSources()) {
|
|
26
|
+
asset.addLocalSourceFromJSON(contentUri = STYLE_SOURCE_CONTENT_URI.toUri())
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The picker thumbnail URI for each style, keyed by style id, read from [STYLE_SOURCE_ID]. Styles
|
|
32
|
+
* without a bundled asset (e.g. the unstyled default) are simply absent from the map.
|
|
33
|
+
*/
|
|
34
|
+
suspend fun Engine.loadStyleThumbnails(): Map<String, String> = asset.findAssets(
|
|
35
|
+
sourceId = STYLE_SOURCE_ID,
|
|
36
|
+
query = FindAssetsQuery(page = 0, perPage = 100),
|
|
37
|
+
).assets.mapNotNull { asset ->
|
|
38
|
+
asset.meta?.get("thumbUri")?.let { asset.id to it }
|
|
39
|
+
}.toMap()
|
|
40
|
+
|
|
41
|
+
/** The backdrop video URI for a style, read from its [STYLE_SOURCE_ID] asset (null if absent). */
|
|
42
|
+
suspend fun Engine.styleBackgroundVideoUri(assetId: String): String? =
|
|
43
|
+
asset.fetchAsset(sourceId = STYLE_SOURCE_ID, assetId = assetId)?.meta?.get("uri")
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
package ly.img.editor.configuration.memories.style
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Catalog of the video styles offered in the Styles sheet.
|
|
5
|
+
*
|
|
6
|
+
* This is the single source of truth for a style: its name, typeface, preferred font
|
|
7
|
+
* weights, the image-filter adjustments, the scene background and media scale, and the title
|
|
8
|
+
* text color all live here. To add, remove, or tune a style, edit [VideoStyles.ALL] below —
|
|
9
|
+
* nothing else needs to change. The generic application logic lives in [StyleApplier].
|
|
10
|
+
*
|
|
11
|
+
* The bundled files a style needs — its looping backdrop [StyleBackground.Video] and its picker
|
|
12
|
+
* thumbnail — are supplied by the [STYLE_SOURCE_ID] custom local asset source (see
|
|
13
|
+
* [StyleAssetSource]). A style references them by [id], never by a hard-coded `file://` path.
|
|
14
|
+
*/
|
|
15
|
+
data class VideoStyle(
|
|
16
|
+
/** Stable lowercase identifier used as the key everywhere (e.g. "noir"). */
|
|
17
|
+
val id: String,
|
|
18
|
+
/** Human-facing name shown in the sheet (e.g. "Noir"). */
|
|
19
|
+
val displayName: String,
|
|
20
|
+
/** Typeface name as it appears in the "ly.img.typeface" asset source. */
|
|
21
|
+
val typeface: String,
|
|
22
|
+
/** Preferred font sub-families, most-preferred first; falls back to the first available. */
|
|
23
|
+
val fontWeights: List<String>,
|
|
24
|
+
/** Adjustment effect properties, e.g. ("effect/adjustments/saturation", -1.0f). Empty = no adjustments. */
|
|
25
|
+
val adjustments: List<Pair<String, Float>> = emptyList(),
|
|
26
|
+
/** How much of the page the media fills. 1.0 = full-bleed; < 1.0 reveals the [background]. */
|
|
27
|
+
val mediaScale: Float = 1f,
|
|
28
|
+
/** The backdrop shown behind the (scaled-down) media. */
|
|
29
|
+
val background: StyleBackground = StyleBackground.None,
|
|
30
|
+
/** Title text color for this style (hex). White reads on the dark/video backdrops; Noir uses black. */
|
|
31
|
+
val titleTextColorHex: String = "#FFFFFF",
|
|
32
|
+
/** Opaque ARGB color for the picker tile (shown behind the icon / as a placeholder). */
|
|
33
|
+
val previewBackground: Long,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
/** The backdrop a style paints behind the media once the media is scaled below full-bleed. */
|
|
37
|
+
sealed interface StyleBackground {
|
|
38
|
+
/** No backdrop: the media fills the whole page (used by the unstyled default). */
|
|
39
|
+
object None : StyleBackground
|
|
40
|
+
|
|
41
|
+
/** A flat color fill behind the media (e.g. white for Noir). [colorHex] is an "#RRGGBB" string. */
|
|
42
|
+
data class Solid(
|
|
43
|
+
val colorHex: String,
|
|
44
|
+
) : StyleBackground
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A looping video behind the media, supplied by the [STYLE_SOURCE_ID] asset source.
|
|
48
|
+
* [assetId] is the id of the backdrop asset in that source (e.g. "hologram").
|
|
49
|
+
*/
|
|
50
|
+
data class Video(
|
|
51
|
+
val assetId: String,
|
|
52
|
+
) : StyleBackground
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
object VideoStyles {
|
|
56
|
+
/** Neutral, unstyled: clean modern sans, full-bleed media, no filter or backdrop. */
|
|
57
|
+
val DEFAULT = VideoStyle(
|
|
58
|
+
id = "default",
|
|
59
|
+
displayName = "Default",
|
|
60
|
+
typeface = "Montserrat",
|
|
61
|
+
fontWeights = listOf("SemiBold", "Medium", "Regular"),
|
|
62
|
+
previewBackground = 0xFFEFEBE9,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
/** Professional black & white on a clean white backdrop, with black title type. */
|
|
66
|
+
val NOIR = VideoStyle(
|
|
67
|
+
id = "noir",
|
|
68
|
+
displayName = "Noir",
|
|
69
|
+
typeface = "Playfair Display",
|
|
70
|
+
fontWeights = listOf("Bold", "SemiBold"),
|
|
71
|
+
adjustments = listOf(
|
|
72
|
+
"effect/adjustments/saturation" to -1.0f,
|
|
73
|
+
"effect/adjustments/contrast" to 0.15f,
|
|
74
|
+
"effect/adjustments/clarity" to 0.1f,
|
|
75
|
+
),
|
|
76
|
+
mediaScale = 0.8f,
|
|
77
|
+
background = StyleBackground.Solid(colorHex = "#FFFFFF"),
|
|
78
|
+
titleTextColorHex = "#000000",
|
|
79
|
+
previewBackground = 0xFF222222,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
/** Futuristic cool cast over a looping hologram backdrop. A blue temperature shift cools the media. */
|
|
83
|
+
val HOLOGRAM = VideoStyle(
|
|
84
|
+
id = "hologram",
|
|
85
|
+
displayName = "Hologram",
|
|
86
|
+
typeface = "VT323",
|
|
87
|
+
fontWeights = listOf("Regular"),
|
|
88
|
+
adjustments = listOf(
|
|
89
|
+
"effect/adjustments/temperature" to -0.4f,
|
|
90
|
+
"effect/adjustments/contrast" to 0.1f,
|
|
91
|
+
),
|
|
92
|
+
mediaScale = 0.8f,
|
|
93
|
+
background = StyleBackground.Video(assetId = "hologram"),
|
|
94
|
+
previewBackground = 0xFFE1F5FE,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
/** Playful, poppy filter over a looping bubblegum backdrop: punchy saturation, bright tones. */
|
|
98
|
+
val BUBBLEGUM = VideoStyle(
|
|
99
|
+
id = "bubblegum",
|
|
100
|
+
displayName = "Bubblegum",
|
|
101
|
+
typeface = "Lobster Two",
|
|
102
|
+
fontWeights = listOf("Bold", "Regular"),
|
|
103
|
+
adjustments = listOf(
|
|
104
|
+
"effect/adjustments/saturation" to 0.5f,
|
|
105
|
+
"effect/adjustments/brightness" to 0.05f,
|
|
106
|
+
"effect/adjustments/contrast" to 0.1f,
|
|
107
|
+
),
|
|
108
|
+
mediaScale = 0.8f,
|
|
109
|
+
background = StyleBackground.Video(assetId = "bubblegum"),
|
|
110
|
+
previewBackground = 0xFFFCE4EC,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
/** All styles, in the order they appear in the Styles sheet. */
|
|
114
|
+
val ALL = listOf(DEFAULT, NOIR, HOLOGRAM, BUBBLEGUM)
|
|
115
|
+
|
|
116
|
+
/** Resolves a style by its [id] (case-insensitive), falling back to [DEFAULT]. */
|
|
117
|
+
fun byId(id: String): VideoStyle = ALL.firstOrNull { it.id.equals(id, ignoreCase = true) } ?: DEFAULT
|
|
118
|
+
}
|