react-native-photo-video-editor 0.3.1 → 0.3.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.2
6
+
7
+ - **Fix iOS video export crash**: Resolved `EXC_BREAKPOINT` / `_xpc_api_misuse` crash on iOS 26+ in `basicvideocompositor.output` by replacing `AVVideoCompositionCoreAnimationTool` with `AVVideoComposition(asset:applyingCIFiltersWithHandler:)` and Core Image compositing in `VideoExporter`.
8
+ - **Fix exported video overlay orientation on iOS**: Removed coordinate inversion that was causing stickers and text overlays to export upside down. Overlays now match preview orientation.
9
+ - **Video editor UI alignment with Android**:
10
+ - Simplified iOS video editor dock to only Text and Stickers tools, matching Android Kotlin.
11
+ - Removed video trim timeline bar from the main editing screen.
12
+ - Fixed layer property slider height, layout constraints, and toolbar spacing to prevent squashing and clipping across all device sizes.
13
+ - **Photo editor bottom toolbar enhancements**:
14
+ - Increased icon size (20pt SF Symbol in 36x36 tile) and font size (12.5pt medium) across the main bottom tool list.
15
+ - Increased button spacing and item width in the horizontal tool rail for improved legibility and touch ergonomics.
16
+ - **Crop panel adjustments**:
17
+ - Reduced font sizes of "Crop" header title (14pt semibold) and "Reset" button (12.5pt medium).
18
+ - Fixed text wrapping on the "Reset" button (previously breaking into `Re-` / `set`).
19
+ - Added insets and adjusted width on aspect ratio chips so "Original" stays on a single line.
20
+ - **Configurable completion button text**:
21
+ - Added `doneButtonText` (and alias `exportButtonText`) prop to `EditorOptions` and `EditorTheme`.
22
+ - Consumers can now pass custom strings (e.g. `'Done'`, `'Export'`, `'Save'`, `'Next'`) to customize the top-right header button on both iOS and Android (defaults to `'Done'`).
23
+ - **Standardized editor header title**:
24
+ - Standardized the default header title to `"Editor"` with 16pt font size on both iOS and Android.
25
+
5
26
  ## 0.3.1
6
27
 
7
28
  - Reduce Android large-video memory pressure with a heap-aware preview sample-buffer target (up to 16 MiB), size-first buffering, and release of preview samples/decoders during export. Restore the paused preview after export cancellation or failure.
package/README.md CHANGED
@@ -40,6 +40,7 @@ try {
40
40
  { id: 'device-sticker', uri: 'file:///path/to/sticker.png' },
41
41
  ],
42
42
  export: { quality: 'high', imageFormat: 'png' },
43
+ doneButtonText: 'Done',
43
44
  saveToGallery: false,
44
45
  });
45
46
  console.log(result.uri, result.cancelled);
@@ -78,6 +78,13 @@ class PhotoVideoEditorActivity : Activity() {
78
78
  private val mediaType by lazy { intent.getStringExtra(EXTRA_TYPE).orEmpty() }
79
79
  private val request by lazy { JSONObject(intent.getStringExtra(EXTRA_REQUEST).orEmpty()) }
80
80
  private val theme by lazy { request.optJSONObject("theme") }
81
+ private val doneButtonText by lazy {
82
+ request.optString("doneButtonText").takeIf { it.isNotBlank() }
83
+ ?: request.optString("exportButtonText").takeIf { it.isNotBlank() }
84
+ ?: theme?.optString("doneButtonText")?.takeIf { it.isNotBlank() }
85
+ ?: theme?.optString("exportButtonText")?.takeIf { it.isNotBlank() }
86
+ ?: "Done"
87
+ }
81
88
 
82
89
  private var photoSession: PhotoEditSession? = null
83
90
  private var cropMode = false
@@ -2547,8 +2554,8 @@ class PhotoVideoEditorActivity : Activity() {
2547
2554
  )
2548
2555
 
2549
2556
  addView(TextView(this@PhotoVideoEditorActivity).apply {
2550
- text = if (mediaType == "photo") "Photo Editor" else "Video Editor"
2551
- textSize = 14f
2557
+ text = "Editor"
2558
+ textSize = 16f
2552
2559
  maxLines = 1
2553
2560
  ellipsize = android.text.TextUtils.TruncateAt.END
2554
2561
  gravity = Gravity.CENTER
@@ -2573,7 +2580,7 @@ class PhotoVideoEditorActivity : Activity() {
2573
2580
 
2574
2581
 
2575
2582
  addView(
2576
- editorPrimaryButton(this@PhotoVideoEditorActivity, "Export", null, primaryColor, onPrimaryColor) {
2583
+ editorPrimaryButton(this@PhotoVideoEditorActivity, doneButtonText, null, primaryColor, onPrimaryColor) {
2577
2584
  if (mediaType == "photo") showPhotoExportConfiguration() else finishDone()
2578
2585
  },
2579
2586
  LinearLayout.LayoutParams(WRAP, dp(DesignTokens.touchTargetMin)).apply { marginStart = dp(DesignTokens.spaceXs) }
@@ -6,6 +6,7 @@ Pass `features` to control which tool entries appear. Photo-only normalization h
6
6
 
7
7
  - `backgroundColor`, `toolbarColor`, `textColor`, `primaryColor` accept `#RRGGBB`/`#AARRGGBB` hex strings. Android parses them with `Color.parseColor`; iOS parses them itself. An unset or malformed value falls back to the original dark shell colors, so partial themes are safe.
8
8
  - `statusBarStyle` (`'light' | 'dark'`) sets the status bar content color (Android `SYSTEM_UI_FLAG_LIGHT_STATUS_BAR`, iOS `preferredStatusBarStyle`).
9
+ - `doneButtonText` (or `exportButtonText`): custom label string for the top-right completion button (defaults to `'Done'`). Can be set on `theme` or directly on top-level `EditorOptions`.
9
10
 
10
11
  Toolbar ordering/grouping beyond the fixed tool list is not yet implemented. The crop aspect-ratio preset list (Free, 1:1, 4:5, 3:4, 9:16, 16:9) and the filter/adjustment list are currently fixed natively and not yet configurable from the TypeScript API — a documented gap for a later milestone.
11
12
 
@@ -20,6 +20,7 @@ final class PhotoVideoEditorViewController: UIViewController {
20
20
  private var initialStickerImages: [String: (URL, UIImage)] = [:]
21
21
  private var initialStickerLoading = false
22
22
  private let stickerAssets: [[String: Any]]
23
+ private let doneButtonText: String
23
24
  var completion: ((PhotoVideoEditorOutcome) -> Void)?
24
25
 
25
26
  private var photoSession: PhotoEditSession?
@@ -90,6 +91,7 @@ final class PhotoVideoEditorViewController: UIViewController {
90
91
  private var videoNaturalSize: CGSize?
91
92
  private var videoLayerOverlay: LayerOverlayView?
92
93
  private var videoLayerToolBar: UIView?
94
+ private var videoToolBarWrapper: UIView?
93
95
  private var videoLayerPropertySlider: UISlider?
94
96
  private var selectedVideoLayerID: String?
95
97
  private var activeVideoLayerPropertyKey = ""
@@ -122,7 +124,8 @@ final class PhotoVideoEditorViewController: UIViewController {
122
124
  theme: [String: Any] = [:],
123
125
  exportOptions: [String: Any] = [:],
124
126
  stickerAssets: [[String: Any]] = [],
125
- initialStickerIds: [String] = []
127
+ initialStickerIds: [String] = [],
128
+ doneButtonText: String? = nil
126
129
  ) {
127
130
  self.uri = uri
128
131
  self.mediaType = mediaType
@@ -131,6 +134,7 @@ final class PhotoVideoEditorViewController: UIViewController {
131
134
  self.exportOptions = exportOptions
132
135
  self.stickerAssets = stickerAssets
133
136
  self.initialStickerIds = initialStickerIds
137
+ self.doneButtonText = doneButtonText ?? (theme["doneButtonText"] as? String) ?? (theme["exportButtonText"] as? String) ?? "Done"
134
138
  super.init(nibName: nil, bundle: nil)
135
139
  modalPresentationStyle = .fullScreen
136
140
  }
@@ -462,7 +466,8 @@ final class PhotoVideoEditorViewController: UIViewController {
462
466
 
463
467
  init(systemName: String, labelText: String, onTap: (() -> Void)? = nil) {
464
468
  self.onTapAction = onTap
465
- self.icon = UIImageView(image: UIImage(systemName: systemName))
469
+ let config = UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)
470
+ self.icon = UIImageView(image: UIImage(systemName: systemName, withConfiguration: config))
466
471
  super.init(frame: .zero)
467
472
  setupViews(labelText: labelText)
468
473
  }
@@ -476,7 +481,7 @@ final class PhotoVideoEditorViewController: UIViewController {
476
481
 
477
482
  tile.isUserInteractionEnabled = false
478
483
  tile.backgroundColor = .clear
479
- tile.layer.cornerRadius = 15
484
+ tile.layer.cornerRadius = 18
480
485
  tile.clipsToBounds = true
481
486
 
482
487
  icon.isUserInteractionEnabled = false
@@ -488,13 +493,13 @@ final class PhotoVideoEditorViewController: UIViewController {
488
493
  NSLayoutConstraint.activate([
489
494
  icon.centerXAnchor.constraint(equalTo: tile.centerXAnchor),
490
495
  icon.centerYAnchor.constraint(equalTo: tile.centerYAnchor),
491
- icon.widthAnchor.constraint(equalToConstant: 18),
492
- icon.heightAnchor.constraint(equalToConstant: 18),
496
+ icon.widthAnchor.constraint(equalToConstant: 22),
497
+ icon.heightAnchor.constraint(equalToConstant: 22),
493
498
  ])
494
499
 
495
500
  label.isUserInteractionEnabled = false
496
501
  label.text = labelText
497
- label.font = .systemFont(ofSize: 11, weight: .medium)
502
+ label.font = .systemFont(ofSize: 12.5, weight: .medium)
498
503
  label.textColor = DesignTokens.textSecondary
499
504
  label.textAlignment = .center
500
505
  label.numberOfLines = 1
@@ -510,12 +515,12 @@ final class PhotoVideoEditorViewController: UIViewController {
510
515
  addSubview(contentStack)
511
516
  contentStack.translatesAutoresizingMaskIntoConstraints = false
512
517
  NSLayoutConstraint.activate([
513
- tile.widthAnchor.constraint(equalToConstant: 30),
514
- tile.heightAnchor.constraint(equalToConstant: 30),
518
+ tile.widthAnchor.constraint(equalToConstant: 36),
519
+ tile.heightAnchor.constraint(equalToConstant: 36),
515
520
  contentStack.centerXAnchor.constraint(equalTo: centerXAnchor),
516
521
  contentStack.centerYAnchor.constraint(equalTo: centerYAnchor),
517
- label.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 2),
518
- label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -2),
522
+ label.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 1),
523
+ label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -1),
519
524
  ])
520
525
 
521
526
  if onTapAction != nil {
@@ -594,7 +599,7 @@ final class PhotoVideoEditorViewController: UIViewController {
594
599
  let stack = UIStackView()
595
600
  stack.axis = .horizontal
596
601
  stack.spacing = 2
597
- let itemWidth: CGFloat = 58
602
+ let itemWidth: CGFloat = 64
598
603
  tools.filter { key, _, _ in
599
604
  if key == "overlay" {
600
605
  return (features["overlay"] as? Bool ?? features["overlays"] as? Bool) != false
@@ -1157,7 +1162,13 @@ final class PhotoVideoEditorViewController: UIViewController {
1157
1162
  private func refreshContextSelection(_ buttons: [String: UIButton], active: String) {
1158
1163
  buttons.forEach { key, control in
1159
1164
  let selected = key == active
1160
- control.configuration?.baseForegroundColor = key == "delete" ? .systemRed : (selected ? (color("primaryColor") ?? DesignTokens.primaryContainer) : DesignTokens.outline)
1165
+ if key == "delete" {
1166
+ control.configuration?.baseForegroundColor = .systemRed
1167
+ } else if key == "done" {
1168
+ control.configuration?.baseForegroundColor = color("primaryColor") ?? DesignTokens.primaryContainer
1169
+ } else {
1170
+ control.configuration?.baseForegroundColor = selected ? (color("primaryColor") ?? DesignTokens.primaryContainer) : DesignTokens.outline
1171
+ }
1161
1172
  control.backgroundColor = selected ? DesignTokens.surfaceContainerHigh : .clear
1162
1173
  control.layer.cornerRadius = 12
1163
1174
  control.accessibilityTraits = selected ? [.button, .selected] : .button
@@ -1830,6 +1841,7 @@ final class PhotoVideoEditorViewController: UIViewController {
1830
1841
 
1831
1842
  // Transparent holder so the safe-area inset lands on empty space rather than stretching the pill.
1832
1843
  let toolbarWrapper = UIView()
1844
+ videoToolBarWrapper = toolbarWrapper
1833
1845
  toolbarWrapper.addSubview(toolbar)
1834
1846
  toolbar.translatesAutoresizingMaskIntoConstraints = false
1835
1847
  NSLayoutConstraint.activate([
@@ -1839,9 +1851,9 @@ final class PhotoVideoEditorViewController: UIViewController {
1839
1851
  toolbar.bottomAnchor.constraint(equalTo: toolbarWrapper.bottomAnchor, constant: -DesignTokens.spaceSm),
1840
1852
  ])
1841
1853
 
1842
- let trimEnabled = features["trim"] as? Bool != false
1843
- trim.isHidden = !trimEnabled
1844
- let root = UIStackView(arrangedSubviews: [header, previewWrapper, trim, cropPanel, aspectBar, layerSlider, palette, layerBar, toolbarWrapper])
1854
+ // Single-video editor: keep the internal range initialized for export,
1855
+ // but do not show the trim/clip timeline UI (matches Android).
1856
+ let root = UIStackView(arrangedSubviews: [header, previewWrapper, cropPanel, aspectBar, layerSlider, palette, layerBar, toolbarWrapper])
1845
1857
  root.axis = .vertical
1846
1858
  root.spacing = DesignTokens.spaceSm
1847
1859
  root.translatesAutoresizingMaskIntoConstraints = false
@@ -1852,8 +1864,9 @@ final class PhotoVideoEditorViewController: UIViewController {
1852
1864
  root.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
1853
1865
  root.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
1854
1866
  header.heightAnchor.constraint(equalToConstant: 60),
1855
- trim.heightAnchor.constraint(equalToConstant: 48),
1856
1867
  aspectBar.heightAnchor.constraint(equalToConstant: 56),
1868
+ layerSlider.heightAnchor.constraint(equalToConstant: 64),
1869
+ palette.heightAnchor.constraint(equalToConstant: 48),
1857
1870
  layerBar.heightAnchor.constraint(equalToConstant: 68),
1858
1871
  toolbar.heightAnchor.constraint(equalToConstant: 52),
1859
1872
  ])
@@ -2060,8 +2073,9 @@ final class PhotoVideoEditorViewController: UIViewController {
2060
2073
  if visible { setVideoControlsVisible(true) }
2061
2074
  videoLayerToolBar?.isHidden = !visible
2062
2075
  videoToolBar?.isHidden = visible
2076
+ videoToolBarWrapper?.isHidden = visible
2063
2077
  let isText = currentSelectedVideoLayer(session: session)?.type == .text
2064
- videoLayerPropertyButtons["scale"]?.isHidden = isText || currentSelectedVideoLayer(session: session)?.type == .sticker
2078
+ videoLayerPropertyButtons["scale"]?.isHidden = true
2065
2079
  ["edit", "color", "fontSize"].forEach { videoLayerPropertyButtons[$0]?.isHidden = !isText }
2066
2080
  if !isText && ["color", "fontSize"].contains(activeVideoLayerPropertyKey) { activeVideoLayerPropertyKey = "" }
2067
2081
  videoLayerPropertySlider?.isHidden = !visible || !["scale", "rotation", "opacity", "fontSize"].contains(activeVideoLayerPropertyKey)
@@ -2092,6 +2106,7 @@ final class PhotoVideoEditorViewController: UIViewController {
2092
2106
  slider.minimumValue = Float(min)
2093
2107
  slider.maximumValue = Float(max)
2094
2108
  slider.value = Float(value)
2109
+ slider.setNeedsDisplay()
2095
2110
  }
2096
2111
 
2097
2112
  @objc private func videoLayerPropertyChanged(_ slider: UISlider) {
@@ -2118,21 +2133,78 @@ final class PhotoVideoEditorViewController: UIViewController {
2118
2133
  }
2119
2134
 
2120
2135
  private func makeVideoLayerToolBar(session: VideoEditSession, textColor: UIColor, primaryColor: UIColor, toolbarColor: UIColor) -> UIView {
2121
- makeContextToolbar(video: true, toolbarColor: toolbarColor) { [weak self] key in
2122
- guard let self, let layer = self.currentSelectedVideoLayer(session: session) else { return }
2123
- switch key {
2124
- case "edit": self.showVideoTextInputDialog(session: session, editingLayer: layer)
2125
- case "done": self.selectVideoLayer(nil, session: session)
2126
- case "delete":
2127
- session.layerStack.commit { $0.filter { $0.id != layer.id } }; self.selectVideoLayer(nil, session: session); self.onVideoLayerStackChanged()
2128
- case "duplicate":
2129
- let copy = layer.duplicated()
2130
- session.layerStack.commit { $0 + [copy] }; self.selectVideoLayer(copy.id, session: session); self.onVideoLayerStackChanged()
2131
- default:
2132
- self.activeVideoLayerPropertyKey = self.activeVideoLayerPropertyKey == key ? "" : key
2133
- self.setVideoLayerToolBarVisible(true, session: session)
2136
+ let bar = UIStackView()
2137
+ bar.axis = .horizontal
2138
+ bar.alignment = .fill
2139
+ bar.distribution = .fillEqually
2140
+ bar.backgroundColor = toolbarColor
2141
+ bar.isLayoutMarginsRelativeArrangement = true
2142
+ bar.layoutMargins = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4)
2143
+
2144
+ let actions: [(String, String, String)] = [
2145
+ ("edit", "Edit", "pencil"),
2146
+ ("color", "Color", "paintpalette"),
2147
+ ("fontSize", "Size", "textformat.size"),
2148
+ ("rotation", "Rotate", "arrow.clockwise"),
2149
+ ("opacity", "Opacity", "circle.lefthalf.filled"),
2150
+ ("duplicate", "Copy", "square.on.square"),
2151
+ ("delete", "Delete", "trash"),
2152
+ ("done", "Done", "checkmark"),
2153
+ ]
2154
+
2155
+ for (key, label, symbol) in actions {
2156
+ let control = makeVideoToolButton(label, symbol: symbol) { [weak self] in
2157
+ guard let self, let layer = self.currentSelectedVideoLayer(session: session) else { return }
2158
+ switch key {
2159
+ case "edit":
2160
+ self.showVideoTextInputDialog(session: session, editingLayer: layer)
2161
+ case "done":
2162
+ self.selectVideoLayer(nil, session: session)
2163
+ case "delete":
2164
+ session.layerStack.commit { $0.filter { $0.id != layer.id } }
2165
+ self.selectVideoLayer(nil, session: session)
2166
+ self.onVideoLayerStackChanged()
2167
+ case "duplicate":
2168
+ let copy = layer.duplicated()
2169
+ session.layerStack.commit { $0 + [copy] }
2170
+ self.selectVideoLayer(copy.id, session: session)
2171
+ self.onVideoLayerStackChanged()
2172
+ default:
2173
+ self.activeVideoLayerPropertyKey = self.activeVideoLayerPropertyKey == key ? "" : key
2174
+ self.setVideoLayerToolBarVisible(true, session: session)
2175
+ }
2176
+ }
2177
+ if key == "done" {
2178
+ control.configuration?.baseForegroundColor = primaryColor
2179
+ } else if key == "delete" {
2180
+ control.configuration?.baseForegroundColor = .systemRed
2134
2181
  }
2182
+ videoLayerPropertyButtons[key] = control
2183
+ bar.addArrangedSubview(control)
2135
2184
  }
2185
+ return bar
2186
+ }
2187
+
2188
+ private func makeVideoToolButton(_ label: String, symbol: String, action: @escaping () -> Void) -> UIButton {
2189
+ let control = UIButton(type: .system)
2190
+ var config = UIButton.Configuration.plain()
2191
+ config.title = label
2192
+ config.image = UIImage(systemName: symbol, withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .regular))
2193
+ config.imagePlacement = .top
2194
+ config.imagePadding = 3
2195
+ config.baseForegroundColor = DesignTokens.outline
2196
+ config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
2197
+ var value = incoming
2198
+ value.font = .systemFont(ofSize: 10, weight: .medium)
2199
+ return value
2200
+ }
2201
+ config.contentInsets = NSDirectionalEdgeInsets(top: 6, leading: 1, bottom: 6, trailing: 1)
2202
+ control.configuration = config
2203
+ control.accessibilityLabel = label
2204
+ control.heightAnchor.constraint(equalToConstant: 68).isActive = true
2205
+ control.addAction(UIAction { _ in action() }, for: .touchUpInside)
2206
+ control.applyPressScale()
2207
+ return control
2136
2208
  }
2137
2209
 
2138
2210
  private func showVideoTextInputDialog(session: VideoEditSession, editingLayer: PhotoLayer? = nil) {
@@ -2164,49 +2236,27 @@ final class PhotoVideoEditorViewController: UIViewController {
2164
2236
  /// `toolButton` — that rail stacks a symbol above a label (72pt of chrome), far more than two
2165
2237
  /// actions warrant. `textformat` renders as "Aa", which reads as typography where a lone "T" does not.
2166
2238
  private func makeVideoToolBar(session: VideoEditSession, playerController: AVPlayerViewController) -> UIView {
2239
+ let dock = UIStackView()
2240
+ dock.axis = .horizontal
2241
+ dock.distribution = .fillEqually
2242
+ dock.alignment = .fill
2243
+ dock.backgroundColor = DesignTokens.surfaceContainer
2244
+ dock.layer.cornerRadius = 18
2245
+ dock.clipsToBounds = true
2246
+
2167
2247
  let tools: [(String, String, String)] = [
2168
- ("trim", "Trim", "arrow.left.and.right"),
2169
- ("crop", "Crop", "crop"),
2170
- ("rotate", "Rotate", "rotate.right"),
2171
- ("speed", "Speed", "gauge.with.dots.needle.50percent"),
2172
2248
  ("text", "Text", "textformat"),
2173
2249
  ("stickers", "Stickers", "face.smiling"),
2174
- ("overlay", "Overlay", "plus.rectangle.on.rectangle"),
2175
- ("cover", "Cover", "photo"),
2176
2250
  ]
2177
- let scroll = UIScrollView()
2178
- scroll.backgroundColor = DesignTokens.surfaceContainer
2179
- scroll.layer.cornerRadius = 18
2180
- scroll.clipsToBounds = true
2181
- scroll.showsHorizontalScrollIndicator = false
2182
-
2183
- let stack = UIStackView()
2184
- stack.axis = .horizontal
2185
- stack.spacing = DesignTokens.spaceXs
2186
-
2187
- let itemWidth = Swift.min(Swift.max(UIScreen.main.bounds.width / 4.5, 76), 96)
2188
2251
  tools.filter { key, _, _ in
2189
- if key == "overlay" {
2190
- return (features["overlay"] as? Bool ?? features["overlays"] as? Bool) != false
2191
- }
2192
- return features[key] as? Bool != false
2252
+ features[key] as? Bool != false
2193
2253
  }.forEach { key, label, symbol in
2194
2254
  let control = makeVideoDockAction(label: label, symbol: symbol) { [weak self, weak playerController] in
2195
2255
  self?.onVideoToolTapped(key, session: session, playerView: playerController?.view)
2196
2256
  }
2197
- control.widthAnchor.constraint(equalToConstant: itemWidth).isActive = true
2198
- stack.addArrangedSubview(control)
2257
+ dock.addArrangedSubview(control)
2199
2258
  }
2200
- scroll.addSubview(stack)
2201
- stack.translatesAutoresizingMaskIntoConstraints = false
2202
- NSLayoutConstraint.activate([
2203
- stack.leadingAnchor.constraint(equalTo: scroll.contentLayoutGuide.leadingAnchor, constant: 8),
2204
- stack.trailingAnchor.constraint(equalTo: scroll.contentLayoutGuide.trailingAnchor, constant: -8),
2205
- stack.topAnchor.constraint(equalTo: scroll.contentLayoutGuide.topAnchor),
2206
- stack.bottomAnchor.constraint(equalTo: scroll.contentLayoutGuide.bottomAnchor),
2207
- stack.heightAnchor.constraint(equalTo: scroll.frameLayoutGuide.heightAnchor),
2208
- ])
2209
- return scroll
2259
+ return dock
2210
2260
  }
2211
2261
 
2212
2262
  /// One dock action: symbol + label on a single row, purple pill while pressed.
@@ -2529,9 +2579,9 @@ final class PhotoVideoEditorViewController: UIViewController {
2529
2579
  close.clipsToBounds = true
2530
2580
 
2531
2581
  let title = UILabel()
2532
- title.text = mediaType == "photo" ? "Photo Editor" : "Video Editor"
2582
+ title.text = "Editor"
2533
2583
  title.textColor = textColor
2534
- title.font = .systemFont(ofSize: 14, weight: .semibold)
2584
+ title.font = .systemFont(ofSize: 16 , weight: .semibold)
2535
2585
  title.adjustsFontSizeToFitWidth = true
2536
2586
  title.minimumScaleFactor = 0.75
2537
2587
  title.textAlignment = .center
@@ -2572,8 +2622,8 @@ final class PhotoVideoEditorViewController: UIViewController {
2572
2622
  }
2573
2623
 
2574
2624
  let onPrimaryColor = color("onPrimaryColor") ?? DesignTokens.onPrimaryContainer
2575
- let export = EditorComponents.primaryButton(
2576
- text: "Export",
2625
+ let done = EditorComponents.primaryButton(
2626
+ text: doneButtonText,
2577
2627
  systemImage: nil,
2578
2628
  fillColor: primaryColor,
2579
2629
  textColor: onPrimaryColor
@@ -2581,7 +2631,7 @@ final class PhotoVideoEditorViewController: UIViewController {
2581
2631
  guard let self else { return }
2582
2632
  if self.mediaType == "photo" { self.showPhotoExportConfiguration() } else { self.doneEditor() }
2583
2633
  }
2584
- header.addArrangedSubview(export)
2634
+ header.addArrangedSubview(done)
2585
2635
 
2586
2636
  title.setContentHuggingPriority(.defaultLow, for: .horizontal)
2587
2637
  return header
@@ -25,11 +25,17 @@ final class CropPanel: UIStackView {
25
25
  return button
26
26
  }
27
27
  let reset = UIButton(type:.system)
28
- var resetStyle = UIButton.Configuration.plain(); resetStyle.title = "Reset"; resetStyle.image = UIImage(systemName:"arrow.counterclockwise")
29
- resetStyle.imagePadding = 6; resetStyle.baseForegroundColor = foreground
28
+ var resetStyle = UIButton.Configuration.plain(); resetStyle.title = "Reset"
29
+ resetStyle.image = UIImage(systemName:"arrow.counterclockwise", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13, weight: .medium))
30
+ resetStyle.imagePadding = 5; resetStyle.baseForegroundColor = foreground
31
+ resetStyle.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
32
+ resetStyle.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
33
+ var output = incoming; output.font = .systemFont(ofSize: 12.5, weight: .medium); return output
34
+ }
30
35
  reset.configuration = resetStyle; reset.addAction(UIAction { _ in onReset() },for:.touchUpInside)
31
- reset.widthAnchor.constraint(equalToConstant:88).isActive = true
32
- let title = UILabel(); title.text = "Crop"; title.font = .systemFont(ofSize:17,weight:.semibold); title.textColor = foreground; title.textAlignment = .center
36
+ reset.titleLabel?.numberOfLines = 1
37
+ reset.widthAnchor.constraint(equalToConstant:76).isActive = true
38
+ let title = UILabel(); title.text = "Crop"; title.font = .systemFont(ofSize:14,weight:.semibold); title.textColor = foreground; title.textAlignment = .center
33
39
  let done = icon("checkmark",label:"Apply crop",action:onDone); done.tintColor = accent
34
40
  let header = UIStackView(arrangedSubviews:[reset,title,icon("xmark",label:"Cancel crop",action:onCancel),done]); header.axis = .horizontal
35
41
  header.heightAnchor.constraint(equalToConstant:48).isActive = true; addArrangedSubview(header)
@@ -48,6 +54,7 @@ final class CropPanel: UIStackView {
48
54
  let control = UIButton(type:.system)
49
55
  var config = UIButton.Configuration.plain(); config.title = name; config.imagePlacement = .top; config.imagePadding = 6
50
56
  config.baseForegroundColor = foreground
57
+ config.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 2, bottom: 4, trailing: 2)
51
58
  config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { value in var output = value; output.font = .systemFont(ofSize:11); return output }
52
59
  let aspect = name == "Original" ? max(0.01,originalRatio()) : ratio ?? 1.25
53
60
  config.image = UIGraphicsImageRenderer(size:CGSize(width:30,height:30)).image { _ in
@@ -57,7 +64,7 @@ final class CropPanel: UIStackView {
57
64
  }.withRenderingMode(.alwaysTemplate)
58
65
  control.configuration = config; control.layer.cornerRadius = 12
59
66
  control.accessibilityLabel = "\(name) crop ratio"
60
- control.widthAnchor.constraint(equalToConstant:64).isActive = true
67
+ control.widthAnchor.constraint(equalToConstant:66).isActive = true
61
68
  control.addAction(UIAction { _ in onRatio(name,name == "Original" ? originalRatio() : ratio) },for:.touchUpInside)
62
69
  control.applyPressScale(); rail.addArrangedSubview(control); presets[name] = control
63
70
  }
@@ -24,6 +24,7 @@ public class PhotoVideoEditorSwift: NSObject {
24
24
  guard !editorOpen else { reject("E_EDITOR_ALREADY_OPEN", "Another editor is already open.", nil); return }
25
25
  guard let presenter = RCTPresentedViewController() else { reject("E_EDITOR_UNAVAILABLE", "The editor requires a foreground view controller.", nil); return }
26
26
  editorOpen = true
27
+ let doneButtonText = payload["doneButtonText"] as? String ?? payload["exportButtonText"] as? String
27
28
  let controller = PhotoVideoEditorViewController(
28
29
  uri: uri,
29
30
  mediaType: type,
@@ -31,7 +32,8 @@ public class PhotoVideoEditorSwift: NSObject {
31
32
  theme: payload["theme"] as? [String: Any] ?? [:],
32
33
  exportOptions: payload["export"] as? [String: Any] ?? [:],
33
34
  stickerAssets: payload["stickerAssets"] as? [[String: Any]] ?? [],
34
- initialStickerIds: payload["initialStickerIds"] as? [String] ?? []
35
+ initialStickerIds: payload["initialStickerIds"] as? [String] ?? [],
36
+ doneButtonText: doneButtonText
35
37
  )
36
38
  controller.completion = { [weak self, weak controller] outcome in
37
39
  guard let self else { return }
@@ -1,4 +1,5 @@
1
1
  import AVFoundation
2
+ import CoreImage
2
3
  import QuartzCore
3
4
  import UIKit
4
5
 
@@ -56,13 +57,17 @@ final class VideoExporter {
56
57
 
57
58
  var sourceVideoTrack: AVAssetTrack?
58
59
  var cursor = CMTime.zero
60
+ var hasAudio = false
59
61
  for clip in clips {
60
62
  let clipAsset = assetProvider(clip)
61
63
  guard let clipVideoTrack = clipAsset.tracks(withMediaType: .video).first else {
62
64
  onError(VideoExportError(code: "E_SOURCE_UNREADABLE", message: "A clip's source video could not be decoded."))
63
65
  return
64
66
  }
65
- if sourceVideoTrack == nil { sourceVideoTrack = clipVideoTrack }
67
+ if sourceVideoTrack == nil {
68
+ sourceVideoTrack = clipVideoTrack
69
+ compositionVideoTrack.preferredTransform = clipVideoTrack.preferredTransform
70
+ }
66
71
  let range = CMTimeRange(
67
72
  start: CMTime(value: clip.trimStartMs, timescale: 1000),
68
73
  end: CMTime(value: clip.effectiveTrimEndMs(), timescale: 1000)
@@ -74,10 +79,15 @@ final class VideoExporter {
74
79
  return
75
80
  }
76
81
  if let compositionAudioTrack, let clipAudioTrack = clipAsset.tracks(withMediaType: .audio).first {
77
- try? compositionAudioTrack.insertTimeRange(range, of: clipAudioTrack, at: cursor)
82
+ if (try? compositionAudioTrack.insertTimeRange(range, of: clipAudioTrack, at: cursor)) != nil {
83
+ hasAudio = true
84
+ }
78
85
  }
79
86
  cursor = CMTimeAdd(cursor, range.duration)
80
87
  }
88
+ if !hasAudio, let compositionAudioTrack {
89
+ composition.removeTrack(compositionAudioTrack)
90
+ }
81
91
  let timeRange = CMTimeRange(start: .zero, duration: cursor)
82
92
  let durationMs = clips.reduce(Int64(0)) { $0 + $1.trimmedDurationMs() }
83
93
 
@@ -133,27 +143,27 @@ final class VideoExporter {
133
143
 
134
144
  let cropPlan = VideoCropGeometry.plan(size:CGSize(width:renderWidth,height:renderHeight),state:state.crop,includeCrop:true)
135
145
  transform = transform.concatenating(cropPlan.transform)
136
- renderWidth = cropPlan.size.width; renderHeight = cropPlan.size.height
146
+ renderWidth = max(2, floor(cropPlan.size.width / 2) * 2)
147
+ renderHeight = max(2, floor(cropPlan.size.height / 2) * 2)
137
148
 
138
- let videoComposition = AVMutableVideoComposition()
139
- videoComposition.renderSize = CGSize(width: renderWidth, height: renderHeight)
140
- videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
149
+ let renderSize = CGSize(width: renderWidth, height: renderHeight)
150
+ let visibleLayers = layers.filter(\.visible)
151
+ let videoComposition: AVVideoComposition
141
152
 
142
- let instruction = AVMutableVideoCompositionInstruction()
143
- instruction.timeRange = CMTimeRange(start: .zero, duration: timeRange.duration)
144
- let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack)
145
- layerInstruction.setTransform(transform, at: .zero)
146
- instruction.layerInstructions = [layerInstruction]
147
- videoComposition.instructions = [instruction]
153
+ if visibleLayers.isEmpty {
154
+ let mutableComposition = AVMutableVideoComposition()
155
+ mutableComposition.renderSize = renderSize
156
+ mutableComposition.frameDuration = CMTime(value: 1, timescale: 30)
148
157
 
149
- if !layers.isEmpty {
150
- let renderSize = CGSize(width: renderWidth, height: renderHeight)
158
+ let instruction = AVMutableVideoCompositionInstruction()
159
+ instruction.timeRange = CMTimeRange(start: .zero, duration: timeRange.duration)
160
+ let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack)
161
+ layerInstruction.setTransform(transform, at: .zero)
162
+ instruction.layerInstructions = [layerInstruction]
163
+ mutableComposition.instructions = [instruction]
164
+ videoComposition = mutableComposition
165
+ } else {
151
166
  let longestEdge = max(renderSize.width, renderSize.height)
152
- // Geometry is independent of this cap: each overlay surface below is a full-`renderSize`
153
- // CALayer with `contentsGravity = .resizeAspect`, so a smaller (same-aspect) image is scaled
154
- // back up to cover the whole frame. The cap only trades sharpness for peak memory. (Android's
155
- // Media3 path has no such scale-to-fit — it composites overlay textures 1:1 in pixels — which
156
- // is why `VideoExporter.kt` has to restore the same invariant explicitly via OverlaySettings.)
157
167
  let overlayScale = min(1, 2560 / max(longestEdge, 1))
158
168
  let overlayRenderSize = CGSize(
159
169
  width: max(2, floor(renderSize.width * overlayScale)),
@@ -161,40 +171,100 @@ final class VideoExporter {
161
171
  )
162
172
  let renderer = PhotoEditSession.pixelRenderer(size: overlayRenderSize)
163
173
  let transparentBase = renderer.image { _ in }
164
- let videoLayer = CALayer()
165
- videoLayer.frame = CGRect(origin: .zero, size: renderSize)
166
- let parentLayer = CALayer()
167
- parentLayer.frame = videoLayer.frame
168
- parentLayer.addSublayer(videoLayer)
169
- let durationSeconds = max(CMTimeGetSeconds(timeRange.duration), 0.001)
170
- let visibleLayers = layers.filter(\.visible)
171
- // Merge only adjacent layers with identical timing. This collapses the
172
- // common full-duration stack to one bitmap without changing z-order.
173
- var timingGroups: [(key: String, layers: [PhotoLayer])] = []
174
- for layer in visibleLayers {
175
- let key = "\(max(0, layer.startMs)):\(layer.effectiveEndMs(durationMs: durationMs))"
176
- if timingGroups.last?.key == key {
177
- timingGroups[timingGroups.count - 1].layers.append(layer)
178
- } else {
179
- timingGroups.append((key, [layer]))
174
+
175
+ let overlayTransform = CGAffineTransform(
176
+ scaleX: renderSize.width / overlayRenderSize.width,
177
+ y: renderSize.height / overlayRenderSize.height
178
+ )
179
+
180
+ let lock = NSLock()
181
+ var overlayCache: [String: CIImage] = [:]
182
+
183
+ let mutableComposition = AVMutableVideoComposition(asset: composition) { request in
184
+ let timeMs = Int64(CMTimeGetSeconds(request.compositionTime) * 1000)
185
+ var source = request.sourceImage
186
+
187
+ if source.extent.origin != .zero {
188
+ source = source.transformed(by: CGAffineTransform(translationX: -source.extent.origin.x, y: -source.extent.origin.y))
180
189
  }
181
- }
182
- for entry in timingGroups {
183
- let group = entry.layers
184
- guard let representative = group.first else { continue }
185
- autoreleasepool {
186
- let image = PhotoLayerRenderer.render(transparentBase, layers: group) { uri in
187
- SourceResolver.resolvePath(sourceUri: uri, tempPrefix: "pve_video_layer_export").flatMap { UIImage(contentsOfFile: $0) }
190
+
191
+ if normalizedRotation != 0 {
192
+ let radians = CGFloat(normalizedRotation) * .pi / 180
193
+ source = source.transformed(by: CGAffineTransform(rotationAngle: radians))
194
+ source = source.transformed(by: CGAffineTransform(translationX: -source.extent.origin.x, y: -source.extent.origin.y))
188
195
  }
189
- let surface = CALayer()
190
- surface.frame = parentLayer.bounds
191
- surface.contents = image.cgImage
192
- surface.contentsGravity = .resizeAspect
193
- applyVisibilityTiming(to: surface, layer: representative, durationMs: durationMs, durationSeconds: durationSeconds)
194
- parentLayer.addSublayer(surface)
196
+
197
+ if let targetRatio = state.aspectRatio, targetRatio > 0 {
198
+ let currentRatio = source.extent.width / source.extent.height
199
+ if currentRatio > targetRatio {
200
+ let croppedWidth = source.extent.height * targetRatio
201
+ let cropRect = CGRect(
202
+ x: source.extent.origin.x + (source.extent.width - croppedWidth) / 2,
203
+ y: source.extent.origin.y,
204
+ width: croppedWidth,
205
+ height: source.extent.height
206
+ )
207
+ source = source.cropped(to: cropRect).transformed(by: CGAffineTransform(translationX: -cropRect.origin.x, y: -cropRect.origin.y))
208
+ } else if currentRatio < targetRatio {
209
+ let croppedHeight = source.extent.width / targetRatio
210
+ let cropRect = CGRect(
211
+ x: source.extent.origin.x,
212
+ y: source.extent.origin.y + (source.extent.height - croppedHeight) / 2,
213
+ width: source.extent.width,
214
+ height: croppedHeight
215
+ )
216
+ source = source.cropped(to: cropRect).transformed(by: CGAffineTransform(translationX: -cropRect.origin.x, y: -cropRect.origin.y))
217
+ }
218
+ }
219
+
220
+ if state.crop.rotationDegrees != 0 || state.crop.straightenDegrees != 0 || state.crop.cropLeft > 0 || state.crop.cropTop > 0 || state.crop.cropRight < 1 || state.crop.cropBottom < 1 {
221
+ let cropPlan = VideoCropGeometry.plan(size: CGSize(width: source.extent.width, height: source.extent.height), state: state.crop, includeCrop: true)
222
+ let cropRect = CGRect(
223
+ x: source.extent.origin.x + (source.extent.width - cropPlan.size.width) / 2,
224
+ y: source.extent.origin.y + (source.extent.height - cropPlan.size.height) / 2,
225
+ width: cropPlan.size.width,
226
+ height: cropPlan.size.height
227
+ )
228
+ source = source.cropped(to: cropRect).transformed(by: CGAffineTransform(translationX: -cropRect.origin.x, y: -cropRect.origin.y))
195
229
  }
230
+
231
+ let activeLayers = visibleLayers.filter { $0.isActive(atMs: timeMs, durationMs: durationMs) }
232
+ if !activeLayers.isEmpty {
233
+ let key = activeLayers.map { "\($0.id)" }.joined(separator: ",")
234
+ lock.lock()
235
+ let cached = overlayCache[key]
236
+ lock.unlock()
237
+
238
+ let overlayCI: CIImage?
239
+ if let cached {
240
+ overlayCI = cached
241
+ } else {
242
+ let image = PhotoLayerRenderer.render(transparentBase, layers: activeLayers) { uri in
243
+ SourceResolver.resolvePath(sourceUri: uri, tempPrefix: "pve_video_layer_export").flatMap { UIImage(contentsOfFile: $0) }
244
+ }
245
+ if let cgImage = image.cgImage {
246
+ let ci = CIImage(cgImage: cgImage).transformed(by: overlayTransform)
247
+ lock.lock()
248
+ overlayCache[key] = ci
249
+ lock.unlock()
250
+ overlayCI = ci
251
+ } else {
252
+ overlayCI = nil
253
+ }
254
+ }
255
+
256
+ if let overlayCI {
257
+ source = overlayCI.composited(over: source)
258
+ }
259
+ }
260
+
261
+ let output = source.cropped(to: CGRect(origin: .zero, size: renderSize))
262
+ request.finish(with: output, context: nil)
196
263
  }
197
- videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer)
264
+
265
+ mutableComposition.renderSize = renderSize
266
+ mutableComposition.frameDuration = CMTime(value: 1, timescale: 30)
267
+ videoComposition = mutableComposition
198
268
  }
199
269
 
200
270
  let outputDir = FileManager.default.temporaryDirectory.appendingPathComponent("photovideoeditor", isDirectory: true)
@@ -203,6 +273,7 @@ final class VideoExporter {
203
273
  outputURL = output
204
274
 
205
275
  guard let session = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) else {
276
+ cleanup()
206
277
  onError(VideoExportError(code: "E_INTERNAL", message: "Unable to create the export session."))
207
278
  return
208
279
  }
@@ -219,7 +290,7 @@ final class VideoExporter {
219
290
 
220
291
  session.exportAsynchronously { [weak self] in
221
292
  guard let self else { return }
222
- self.progressTimer?.invalidate()
293
+ self.cleanup()
223
294
  switch session.status {
224
295
  case .completed:
225
296
  self.outputURL = nil
@@ -245,40 +316,14 @@ final class VideoExporter {
245
316
  }
246
317
  }
247
318
 
248
- private func applyVisibilityTiming(to surface: CALayer, layer: PhotoLayer, durationMs: Int64, durationSeconds: Double) {
249
- let start = min(max(layer.startMs, 0), durationMs)
250
- let end = layer.effectiveEndMs(durationMs: durationMs)
251
- if start == 0 && end >= durationMs {
252
- surface.opacity = 1
253
- return
254
- }
255
- let animation = CAKeyframeAnimation(keyPath: "opacity")
256
- var values: [NSNumber] = []
257
- var times: [NSNumber] = []
258
- if start > 0 {
259
- values.append(0); times.append(0)
260
- values.append(1); times.append(NSNumber(value: Double(start) / Double(max(durationMs, 1))))
261
- } else {
262
- values.append(1); times.append(0)
263
- }
264
- if end < durationMs {
265
- values.append(0); times.append(NSNumber(value: Double(end) / Double(max(durationMs, 1))))
266
- } else {
267
- values.append(1); times.append(1)
268
- }
269
- animation.values = values
270
- animation.keyTimes = times
271
- animation.calculationMode = .discrete
272
- animation.beginTime = AVCoreAnimationBeginTimeAtZero
273
- animation.duration = durationSeconds
274
- animation.fillMode = .both
275
- animation.isRemovedOnCompletion = false
276
- surface.add(animation, forKey: "overlayVisibility")
319
+ private func cleanup() {
320
+ progressTimer?.invalidate()
321
+ progressTimer = nil
277
322
  }
278
323
 
279
324
  func cancel() {
280
325
  exportSession?.cancelExport()
281
- progressTimer?.invalidate()
326
+ cleanup()
282
327
  if let outputURL { try? FileManager.default.removeItem(at: outputURL) }
283
328
  }
284
329
  }
@@ -33,6 +33,8 @@ export interface EditorTheme {
33
33
  /** Toolbar color. */ toolbarColor?: string;
34
34
  /** Foreground text color. */ textColor?: string;
35
35
  /** Status bar content style. */ statusBarStyle?: 'light' | 'dark';
36
+ /** Custom label for the top-right completion/export button (defaults to "Done"). */ doneButtonText?: string;
37
+ /** Alias for doneButtonText. */ exportButtonText?: string;
36
38
  }
37
39
  /** Export preferences. The milestone-one placeholder does not export media. */
38
40
  export interface ExportOptions {
@@ -61,6 +63,8 @@ export interface EditorOptions {
61
63
  /** Enabled tools. */ features?: EditorFeatures;
62
64
  /** Native UI theme. */ theme?: EditorTheme;
63
65
  /** Export preferences. */ export?: ExportOptions;
66
+ /** Custom label for the top-right completion/export button (defaults to "Done"). */ doneButtonText?: string;
67
+ /** Alias for doneButtonText. */ exportButtonText?: string;
64
68
  /** Requests saving to the gallery. Currently ignored by the placeholder. */ saveToGallery?: boolean;
65
69
  /** Ordered, unique IDs in stickerAssets. The first starts centered; the sticker’s top-right control cycles through the rest. */ initialStickerIds?: string[];
66
70
  /** Consumer-provided stickers offered by the Stickers tool, in addition to the small built-in set and end-user uploads. */ stickerAssets?: StickerAsset[];
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAC1C,uCAAuC;AACvC,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;AACnE,wCAAwC;AACxC,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;AACxD,wCAAwC;AACxC,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,KAAK,CAAC;AAC9C,sDAAsD;AACtD,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,WAAW,GACX,SAAS,GACT,WAAW,GACX,WAAW,GACX,WAAW,GACX,QAAQ,CAAC;AAEb,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,8BAA8B,CAAC,GAAG,EAAE,MAAM,CAAC;IAC3C,kBAAkB,CAAC,IAAI,EAAE,SAAS,CAAC;IACnC,+BAA+B,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClD,iCAAiC,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrD;AACD,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,wBAAwB,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxC,wBAAwB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1C,8BAA8B,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAC9C,uBAAuB,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC1C,6BAA6B,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAC7C,yEAAyE,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC7F,0EAA0E,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAC1F,oDAAoD,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACzE;AACD,+CAA+C;AAC/C,MAAM,WAAW,WAAW;IAC1B,+BAA+B,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtD,+BAA+B,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzD,qBAAqB,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC5C,6BAA6B,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IACjD,gCAAgC,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACnE,oFAAoF,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAC7G,gCAAgC,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC5D;AACD,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,sBAAsB,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IAC/C,+CAA+C,CAAC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChF,+CAA+C,CAAC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChF,wDAAwD,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3E,yDAAyD,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC7E,0DAA0D,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IACjF,+DAA+D,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnF,0DAA0D,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CACvF;AACD,4FAA4F;AAC5F,MAAM,WAAW,SAAS;IACxB,mEAAmE,CAAC,MAAM,EAAE,MAAM,CAAC;IACnF,wDAAwD,CAAC,GAAG,EAAE,MAAM,CAAC;CACtE;AACD,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,wEAAwE,CAAC,EAAE,EAAE,MAAM,CAAC;IACpF,8GAA8G,CAAC,GAAG,EAAE,MAAM,CAAC;CAC5H;AACD,8CAA8C;AAC9C,MAAM,WAAW,aAAa;IAC5B,4BAA4B,CAAC,MAAM,EAAE,YAAY,CAAC;IAClD,qBAAqB,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAChD,uBAAuB,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC5C,0BAA0B,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAClD,oFAAoF,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAC7G,gCAAgC,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC3D,4EAA4E,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACrG,gIAAgI,CAAC,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9J,2HAA2H,CAAC,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3J,6DAA6D,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;CACnF;AACD,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC;IACvE,MAAM,EAAE,YAAY,GAAG;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1C;AACD,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC;IACvE,MAAM,EAAE,YAAY,GAAG;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1C;AACD,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,aAAa,GACb,kBAAkB,GAClB,gBAAgB,GAChB,eAAe,GACf,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,cAAc,GACd,OAAO,CAAC;AACZ,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,eAAe,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AACD,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;AAC/D,8CAA8C;AAC9C,MAAM,WAAW,YAAY;IAC3B,wBAAwB,CAAC,GAAG,EAAE,MAAM,CAAC;IACrC,yBAAyB,CAAC,IAAI,EAAE,SAAS,CAAC;IAC1C,4BAA4B,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC/C,kCAAkC,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAClD,mCAAmC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACpD,iDAAiD,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IACpE,qCAAqC,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IACxD,4CAA4C,CAAC,SAAS,EAAE,OAAO,CAAC;CACjE;AACD,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,6BAA6B,CAAC,KAAK,EAAE,MAAM,CAAC;IAC5C,sCAAsC,CAAC,QAAQ,EAAE,MAAM,CAAC;IACxD,+BAA+B,CAAC,MAAM,EAAE,YAAY,CAAC;CACtD;AACD,oCAAoC;AACpC,MAAM,WAAW,kBAAkB;IACjC,uDAAuD,CAAC,MAAM,IAAI,IAAI,CAAC;CACxE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-photo-video-editor",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Native photo and video editor for React Native, powered by Kotlin and Swift.",
5
5
  "main": "./lib/module/index.js",
6
6
  "module": "./lib/module/index.js",
package/src/types.ts CHANGED
@@ -41,6 +41,8 @@ export interface EditorTheme {
41
41
  /** Toolbar color. */ toolbarColor?: string;
42
42
  /** Foreground text color. */ textColor?: string;
43
43
  /** Status bar content style. */ statusBarStyle?: 'light' | 'dark';
44
+ /** Custom label for the top-right completion/export button (defaults to "Done"). */ doneButtonText?: string;
45
+ /** Alias for doneButtonText. */ exportButtonText?: string;
44
46
  }
45
47
  /** Export preferences. The milestone-one placeholder does not export media. */
46
48
  export interface ExportOptions {
@@ -69,6 +71,8 @@ export interface EditorOptions {
69
71
  /** Enabled tools. */ features?: EditorFeatures;
70
72
  /** Native UI theme. */ theme?: EditorTheme;
71
73
  /** Export preferences. */ export?: ExportOptions;
74
+ /** Custom label for the top-right completion/export button (defaults to "Done"). */ doneButtonText?: string;
75
+ /** Alias for doneButtonText. */ exportButtonText?: string;
72
76
  /** Requests saving to the gallery. Currently ignored by the placeholder. */ saveToGallery?: boolean;
73
77
  /** Ordered, unique IDs in stickerAssets. The first starts centered; the sticker’s top-right control cycles through the rest. */ initialStickerIds?: string[];
74
78
  /** Consumer-provided stickers offered by the Stickers tool, in addition to the small built-in set and end-user uploads. */ stickerAssets?: StickerAsset[];