react-native-photo-video-editor 0.2.0 → 0.2.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 +17 -0
- package/README.md +17 -0
- package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +45 -0
- package/android/src/main/java/com/photovideoeditor/files/SourceResolver.kt +7 -0
- package/ios/Editor/PhotoVideoEditorViewController.swift +397 -149
- package/ios/Editor/UI/CropPanel.swift +10 -3
- package/ios/Editor/UI/EditorComponents.swift +53 -10
- package/ios/Editor/UI/EditorPanel.swift +228 -155
- package/ios/Editor/UI/EditorToolSlider.swift +4 -2
- package/ios/Editor/UI/TextEditorSheet.swift +4 -4
- package/ios/Files/SourceResolver.swift +131 -3
- package/ios/Photo/Export/PhotoExporter.swift +2 -2
- package/ios/Photo/Rendering/PhotoAdjustmentRenderer.swift +57 -63
- package/ios/Photo/Rendering/PixelBuffer.swift +24 -18
- package/ios/Photo/UI/FilterThumbnailLoader.swift +3 -3
- package/ios/PhotoVideoEditor.mm +33 -2
- package/ios/PhotoVideoEditor.swift +22 -18
- package/ios/Stickers/OnlineStickerSheetViewController.swift +1 -1
- package/ios/Video/Render/VideoEditSession.swift +30 -11
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +2 -1
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +20 -0
- package/src/types.ts +2 -1
|
@@ -61,11 +61,18 @@ final class CropPanel: UIStackView {
|
|
|
61
61
|
control.addAction(UIAction { _ in onRatio(name,name == "Original" ? originalRatio() : ratio) },for:.touchUpInside)
|
|
62
62
|
control.applyPressScale(); rail.addArrangedSubview(control); presets[name] = control
|
|
63
63
|
}
|
|
64
|
-
let scroll = UIScrollView()
|
|
64
|
+
let scroll = UIScrollView()
|
|
65
|
+
scroll.showsHorizontalScrollIndicator = false
|
|
66
|
+
scroll.alwaysBounceHorizontal = true
|
|
67
|
+
scroll.delaysContentTouches = true
|
|
68
|
+
scroll.canCancelContentTouches = true
|
|
69
|
+
scroll.addSubview(rail)
|
|
65
70
|
rail.translatesAutoresizingMaskIntoConstraints = false
|
|
66
71
|
NSLayoutConstraint.activate([
|
|
67
|
-
rail.leadingAnchor.constraint(equalTo:scroll.contentLayoutGuide.leadingAnchor),
|
|
68
|
-
|
|
72
|
+
rail.leadingAnchor.constraint(equalTo:scroll.contentLayoutGuide.leadingAnchor),
|
|
73
|
+
scroll.contentLayoutGuide.trailingAnchor.constraint(equalTo:rail.trailingAnchor),
|
|
74
|
+
rail.topAnchor.constraint(equalTo:scroll.contentLayoutGuide.topAnchor),
|
|
75
|
+
scroll.contentLayoutGuide.bottomAnchor.constraint(equalTo:rail.bottomAnchor),
|
|
69
76
|
rail.heightAnchor.constraint(equalTo:scroll.frameLayoutGuide.heightAnchor), scroll.heightAnchor.constraint(equalToConstant:72)
|
|
70
77
|
])
|
|
71
78
|
addArrangedSubview(scroll)
|
|
@@ -17,10 +17,11 @@ final class ClosureTapGestureRecognizer: UITapGestureRecognizer {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/// Tactile press feedback (scale 1.0 -> 0.97) matching DESIGN.md's "Tactile Instrument Feel".
|
|
20
|
-
///
|
|
21
|
-
///
|
|
22
|
-
///
|
|
23
|
-
|
|
20
|
+
/// Tactile press feedback (scale 1.0 -> 0.97) matching DESIGN.md's "Tactile Instrument Feel".
|
|
21
|
+
/// For UIControl instances (like UIButton), hooks natively into touch tracking without gesture recognizers.
|
|
22
|
+
/// For generic UIViews, uses a non-exclusive long-press recognizer that allows simultaneous recognition
|
|
23
|
+
/// with all tap and scroll gestures.
|
|
24
|
+
private final class EditorPressFeedback: NSObject, UIGestureRecognizerDelegate {
|
|
24
25
|
private let pressedScale: CGFloat
|
|
25
26
|
private weak var target: UIView?
|
|
26
27
|
|
|
@@ -28,19 +29,61 @@ private final class EditorPressFeedback: NSObject {
|
|
|
28
29
|
self.target = target
|
|
29
30
|
self.pressedScale = pressedScale
|
|
30
31
|
super.init()
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
if let control = target as? UIControl {
|
|
33
|
+
control.addAction(UIAction { [weak target] _ in
|
|
34
|
+
UIView.animate(
|
|
35
|
+
withDuration: 0.08,
|
|
36
|
+
delay: 0,
|
|
37
|
+
options: [.allowUserInteraction, .beginFromCurrentState],
|
|
38
|
+
animations: { target?.transform = CGAffineTransform(scaleX: pressedScale, y: pressedScale) }
|
|
39
|
+
)
|
|
40
|
+
}, for: [.touchDown, .touchDragEnter])
|
|
41
|
+
control.addAction(UIAction { [weak target] _ in
|
|
42
|
+
UIView.animate(
|
|
43
|
+
withDuration: 0.12,
|
|
44
|
+
delay: 0,
|
|
45
|
+
options: [.allowUserInteraction, .beginFromCurrentState],
|
|
46
|
+
animations: { target?.transform = .identity }
|
|
47
|
+
)
|
|
48
|
+
}, for: [.touchUpInside, .touchUpOutside, .touchCancel, .touchDragExit])
|
|
49
|
+
} else {
|
|
50
|
+
let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(handle(_:)))
|
|
51
|
+
recognizer.minimumPressDuration = 0
|
|
52
|
+
recognizer.cancelsTouchesInView = false
|
|
53
|
+
recognizer.delegate = self
|
|
54
|
+
target.addGestureRecognizer(recognizer)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
67
|
+
return false
|
|
35
68
|
}
|
|
36
69
|
|
|
37
70
|
@objc private func handle(_ recognizer: UILongPressGestureRecognizer) {
|
|
38
71
|
guard let target else { return }
|
|
39
72
|
switch recognizer.state {
|
|
40
73
|
case .began:
|
|
41
|
-
UIView.animate(
|
|
74
|
+
UIView.animate(
|
|
75
|
+
withDuration: 0.08,
|
|
76
|
+
delay: 0,
|
|
77
|
+
options: [.allowUserInteraction, .beginFromCurrentState],
|
|
78
|
+
animations: { target.transform = CGAffineTransform(scaleX: self.pressedScale, y: self.pressedScale) }
|
|
79
|
+
)
|
|
42
80
|
case .ended, .cancelled, .failed:
|
|
43
|
-
UIView.animate(
|
|
81
|
+
UIView.animate(
|
|
82
|
+
withDuration: 0.12,
|
|
83
|
+
delay: 0,
|
|
84
|
+
options: [.allowUserInteraction, .beginFromCurrentState],
|
|
85
|
+
animations: { target.transform = .identity }
|
|
86
|
+
)
|
|
44
87
|
default:
|
|
45
88
|
break
|
|
46
89
|
}
|
|
@@ -10,8 +10,8 @@ enum EditorPanelMetrics {
|
|
|
10
10
|
static let headerHeight: CGFloat = 40
|
|
11
11
|
static let sliderHeight: CGFloat = 48
|
|
12
12
|
static let stripHeight: CGFloat = 76
|
|
13
|
-
static let stripItemWidth: CGFloat =
|
|
14
|
-
static let iconSize: CGFloat =
|
|
13
|
+
static let stripItemWidth: CGFloat = 58
|
|
14
|
+
static let iconSize: CGFloat = 20
|
|
15
15
|
static let thumbnailWidth: CGFloat = 64
|
|
16
16
|
static let thumbnailHeight: CGFloat = 72
|
|
17
17
|
static var totalHeight: CGFloat { headerHeight + sliderHeight + stripHeight }
|
|
@@ -26,36 +26,52 @@ func editorPanelHeader(
|
|
|
26
26
|
onSecondary: (() -> Void)? = nil,
|
|
27
27
|
onDone: @escaping () -> Void
|
|
28
28
|
) -> UIView {
|
|
29
|
+
let container = UIStackView()
|
|
30
|
+
container.axis = .horizontal
|
|
31
|
+
container.alignment = .center
|
|
32
|
+
container.distribution = .equalSpacing
|
|
33
|
+
container.isLayoutMarginsRelativeArrangement = true
|
|
34
|
+
container.layoutMargins = UIEdgeInsets(
|
|
35
|
+
top: 0,
|
|
36
|
+
left: DesignTokens.spaceMd,
|
|
37
|
+
bottom: 0,
|
|
38
|
+
right: DesignTokens.spaceMd
|
|
39
|
+
)
|
|
40
|
+
|
|
29
41
|
let titleLabel = UILabel()
|
|
30
42
|
titleLabel.text = title
|
|
31
|
-
titleLabel.font = .systemFont(ofSize:
|
|
43
|
+
titleLabel.font = .systemFont(ofSize: 15, weight: .semibold)
|
|
32
44
|
titleLabel.textColor = DesignTokens.textPrimary
|
|
45
|
+
container.addArrangedSubview(titleLabel)
|
|
33
46
|
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
row.isLayoutMarginsRelativeArrangement = true
|
|
39
|
-
row.layoutMargins = UIEdgeInsets(top: 0, left: DesignTokens.spaceLg, bottom: 0, right: DesignTokens.spaceSm)
|
|
40
|
-
|
|
41
|
-
let spacer = UIView()
|
|
42
|
-
spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
43
|
-
row.addArrangedSubview(spacer)
|
|
47
|
+
let actions = UIStackView()
|
|
48
|
+
actions.axis = .horizontal
|
|
49
|
+
actions.spacing = DesignTokens.spaceMd
|
|
50
|
+
actions.alignment = .center
|
|
44
51
|
|
|
45
52
|
if let secondaryLabel, let onSecondary {
|
|
46
|
-
|
|
53
|
+
let reset = UIButton(type: .system)
|
|
54
|
+
reset.setTitle(secondaryLabel, for: .normal)
|
|
55
|
+
reset.setTitleColor(DesignTokens.textSecondary, for: .normal)
|
|
56
|
+
reset.titleLabel?.font = .systemFont(ofSize: 13, weight: .regular)
|
|
57
|
+
reset.addAction(UIAction { _ in onSecondary() }, for: .touchUpInside)
|
|
58
|
+
reset.applyPressScale()
|
|
59
|
+
actions.addArrangedSubview(reset)
|
|
47
60
|
}
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
|
|
62
|
+
let done = editorPanelDoneButton(accentColor: accentColor, action: onDone)
|
|
63
|
+
actions.addArrangedSubview(done)
|
|
64
|
+
|
|
65
|
+
container.addArrangedSubview(actions)
|
|
66
|
+
return container
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
private func editorHeaderAction(label: String, color: UIColor, action: @escaping () -> Void) -> UIButton {
|
|
69
|
+
private func editorPanelDoneButton(accentColor: UIColor, action: @escaping () -> Void) -> UIButton {
|
|
54
70
|
let control = UIButton(type: .system)
|
|
55
71
|
var config = UIButton.Configuration.plain()
|
|
56
|
-
config.title =
|
|
57
|
-
config.baseForegroundColor =
|
|
58
|
-
config.contentInsets = NSDirectionalEdgeInsets(top:
|
|
72
|
+
config.title = "Done"
|
|
73
|
+
config.baseForegroundColor = accentColor
|
|
74
|
+
config.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)
|
|
59
75
|
config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
|
|
60
76
|
var value = incoming; value.font = .systemFont(ofSize: 13, weight: .bold); return value
|
|
61
77
|
}
|
|
@@ -71,180 +87,237 @@ private func editorHeaderAction(label: String, color: UIColor, action: @escaping
|
|
|
71
87
|
func editorToolStrip(_ content: UIStackView) -> UIScrollView {
|
|
72
88
|
let scroll = UIScrollView()
|
|
73
89
|
scroll.showsHorizontalScrollIndicator = false
|
|
90
|
+
scroll.alwaysBounceHorizontal = true
|
|
91
|
+
scroll.delaysContentTouches = true
|
|
92
|
+
scroll.canCancelContentTouches = true
|
|
74
93
|
content.axis = .horizontal
|
|
75
|
-
content.alignment = .
|
|
94
|
+
content.alignment = .fill
|
|
76
95
|
scroll.addSubview(content)
|
|
77
96
|
content.translatesAutoresizingMaskIntoConstraints = false
|
|
78
97
|
NSLayoutConstraint.activate([
|
|
79
98
|
content.leadingAnchor.constraint(equalTo: scroll.contentLayoutGuide.leadingAnchor, constant: DesignTokens.spaceMd),
|
|
80
|
-
|
|
99
|
+
scroll.contentLayoutGuide.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: DesignTokens.spaceMd),
|
|
81
100
|
content.topAnchor.constraint(equalTo: scroll.contentLayoutGuide.topAnchor),
|
|
82
|
-
|
|
101
|
+
scroll.contentLayoutGuide.bottomAnchor.constraint(equalTo: content.bottomAnchor),
|
|
83
102
|
content.heightAnchor.constraint(equalTo: scroll.frameLayoutGuide.heightAnchor),
|
|
84
103
|
])
|
|
85
104
|
return scroll
|
|
86
105
|
}
|
|
87
106
|
|
|
88
|
-
///
|
|
89
|
-
final class
|
|
90
|
-
let
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
private
|
|
95
|
-
|
|
96
|
-
init(
|
|
97
|
-
self.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
/// Interactive icon + label button in an adjustment strip, with native press feedback.
|
|
108
|
+
final class EditorStripItemControl: UIButton {
|
|
109
|
+
let iconHolder = UIView()
|
|
110
|
+
let icon = UIImageView()
|
|
111
|
+
let label = UILabel()
|
|
112
|
+
let dot = UIView()
|
|
113
|
+
private var action: (() -> Void)?
|
|
114
|
+
|
|
115
|
+
init(systemName: String, labelText: String, action: @escaping () -> Void) {
|
|
116
|
+
self.action = action
|
|
117
|
+
super.init(frame: .zero)
|
|
118
|
+
setupViews(systemName: systemName, labelText: labelText)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
122
|
+
|
|
123
|
+
private func setupViews(systemName: String, labelText: String) {
|
|
124
|
+
isAccessibilityElement = true
|
|
125
|
+
accessibilityLabel = labelText
|
|
126
|
+
accessibilityTraits = .button
|
|
127
|
+
|
|
128
|
+
icon.image = UIImage(systemName: systemName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .regular))
|
|
129
|
+
icon.tintColor = DesignTokens.textSecondary
|
|
130
|
+
icon.contentMode = .scaleAspectFit
|
|
131
|
+
icon.isUserInteractionEnabled = false
|
|
132
|
+
|
|
133
|
+
iconHolder.layer.cornerRadius = 15
|
|
134
|
+
iconHolder.isUserInteractionEnabled = false
|
|
135
|
+
iconHolder.addSubview(icon)
|
|
136
|
+
icon.translatesAutoresizingMaskIntoConstraints = false
|
|
137
|
+
NSLayoutConstraint.activate([
|
|
138
|
+
iconHolder.widthAnchor.constraint(equalToConstant: 30),
|
|
139
|
+
iconHolder.heightAnchor.constraint(equalToConstant: 30),
|
|
140
|
+
icon.centerXAnchor.constraint(equalTo: iconHolder.centerXAnchor),
|
|
141
|
+
icon.centerYAnchor.constraint(equalTo: iconHolder.centerYAnchor),
|
|
142
|
+
icon.widthAnchor.constraint(equalToConstant: EditorPanelMetrics.iconSize),
|
|
143
|
+
icon.heightAnchor.constraint(equalToConstant: EditorPanelMetrics.iconSize),
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
label.text = labelText
|
|
147
|
+
label.font = .systemFont(ofSize: 11, weight: .medium)
|
|
148
|
+
label.textColor = DesignTokens.textSecondary
|
|
149
|
+
label.textAlignment = .center
|
|
150
|
+
label.numberOfLines = 1
|
|
151
|
+
label.adjustsFontSizeToFitWidth = true
|
|
152
|
+
label.minimumScaleFactor = 0.85
|
|
153
|
+
label.isUserInteractionEnabled = false
|
|
154
|
+
|
|
155
|
+
dot.layer.cornerRadius = 2
|
|
156
|
+
dot.isHidden = true
|
|
157
|
+
dot.isUserInteractionEnabled = false
|
|
158
|
+
dot.widthAnchor.constraint(equalToConstant: 4).isActive = true
|
|
159
|
+
dot.heightAnchor.constraint(equalToConstant: 4).isActive = true
|
|
160
|
+
|
|
161
|
+
let column = UIStackView(arrangedSubviews: [iconHolder, label, dot])
|
|
162
|
+
column.axis = .vertical
|
|
163
|
+
column.alignment = .center
|
|
164
|
+
column.spacing = DesignTokens.spaceXs
|
|
165
|
+
column.isUserInteractionEnabled = false
|
|
166
|
+
|
|
167
|
+
addSubview(column)
|
|
168
|
+
column.translatesAutoresizingMaskIntoConstraints = false
|
|
169
|
+
NSLayoutConstraint.activate([
|
|
170
|
+
widthAnchor.constraint(equalToConstant: EditorPanelMetrics.stripItemWidth),
|
|
171
|
+
column.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
172
|
+
column.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
173
|
+
column.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor),
|
|
174
|
+
column.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
|
|
175
|
+
])
|
|
176
|
+
|
|
177
|
+
addAction(UIAction { [weak self] _ in self?.action?() }, for: .touchUpInside)
|
|
178
|
+
applyPressScale()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
182
|
+
return bounds.insetBy(dx: -8, dy: -8).contains(point)
|
|
102
183
|
}
|
|
103
184
|
|
|
104
|
-
/// Selected state is a purple icon + label over a faint purple disc — never a filled purple tile,
|
|
105
|
-
/// which at this size reads as a giant button rather than a selection.
|
|
106
185
|
func setSelected(_ selected: Bool, accentColor: UIColor) {
|
|
107
186
|
icon.tintColor = selected ? accentColor : DesignTokens.textSecondary
|
|
108
187
|
label.textColor = selected ? accentColor : DesignTokens.textSecondary
|
|
109
188
|
iconHolder.backgroundColor = selected ? accentColor.withAlphaComponent(0.18) : .clear
|
|
189
|
+
accessibilityTraits = selected ? [.button, .selected] : .button
|
|
110
190
|
}
|
|
111
191
|
|
|
112
|
-
/// Tiny dot marking an adjustment that is no longer at its default value.
|
|
113
192
|
func setModified(_ modified: Bool, accentColor: UIColor) {
|
|
114
193
|
dot.isHidden = !modified
|
|
115
194
|
dot.backgroundColor = accentColor
|
|
116
195
|
}
|
|
117
196
|
}
|
|
118
197
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
198
|
+
/// An icon + single-line label entry in a strip, with an optional "value changed" dot.
|
|
199
|
+
final class EditorStripItem {
|
|
200
|
+
let control: EditorStripItemControl
|
|
201
|
+
var root: UIView { control }
|
|
123
202
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
203
|
+
init(control: EditorStripItemControl) {
|
|
204
|
+
self.control = control
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func setSelected(_ selected: Bool, accentColor: UIColor) {
|
|
208
|
+
control.setSelected(selected, accentColor: accentColor)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func setModified(_ modified: Bool, accentColor: UIColor) {
|
|
212
|
+
control.setModified(modified, accentColor: accentColor)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
136
215
|
|
|
216
|
+
func editorStripItem(systemName: String, labelText: String, action: @escaping () -> Void) -> EditorStripItem {
|
|
217
|
+
let control = EditorStripItemControl(systemName: systemName, labelText: labelText, action: action)
|
|
218
|
+
return EditorStripItem(control: control)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// Interactive filter thumbnail button with native touch actions and selection border.
|
|
222
|
+
final class EditorFilterThumbnailControl: UIButton {
|
|
223
|
+
let image = UIImageView()
|
|
224
|
+
let frameView = UIView()
|
|
137
225
|
let label = UILabel()
|
|
138
|
-
|
|
139
|
-
label.font = .systemFont(ofSize: 11, weight: .medium)
|
|
140
|
-
label.textColor = DesignTokens.textSecondary
|
|
141
|
-
label.textAlignment = .center
|
|
142
|
-
// Labels like "Brightness" and "Saturation" must never wrap to a second line.
|
|
143
|
-
label.numberOfLines = 1
|
|
144
|
-
label.adjustsFontSizeToFitWidth = true
|
|
145
|
-
label.minimumScaleFactor = 0.85
|
|
226
|
+
private var action: (() -> Void)?
|
|
146
227
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
228
|
+
init(labelText: String, action: @escaping () -> Void) {
|
|
229
|
+
self.action = action
|
|
230
|
+
super.init(frame: .zero)
|
|
231
|
+
setupViews(labelText: labelText)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
235
|
+
|
|
236
|
+
private func setupViews(labelText: String) {
|
|
237
|
+
isAccessibilityElement = true
|
|
238
|
+
accessibilityLabel = labelText
|
|
239
|
+
accessibilityTraits = .button
|
|
240
|
+
|
|
241
|
+
image.contentMode = .scaleAspectFill
|
|
242
|
+
image.clipsToBounds = true
|
|
243
|
+
image.layer.cornerRadius = DesignTokens.radiusMd
|
|
244
|
+
image.backgroundColor = DesignTokens.surfaceContainerHigh
|
|
245
|
+
image.isUserInteractionEnabled = false
|
|
246
|
+
|
|
247
|
+
frameView.layer.cornerRadius = DesignTokens.radiusMd + 2
|
|
248
|
+
frameView.layer.borderWidth = 2
|
|
249
|
+
frameView.layer.borderColor = UIColor.clear.cgColor
|
|
250
|
+
frameView.isUserInteractionEnabled = false
|
|
251
|
+
|
|
252
|
+
frameView.addSubview(image)
|
|
253
|
+
image.translatesAutoresizingMaskIntoConstraints = false
|
|
254
|
+
NSLayoutConstraint.activate([
|
|
255
|
+
image.widthAnchor.constraint(equalToConstant: EditorPanelMetrics.thumbnailWidth),
|
|
256
|
+
image.heightAnchor.constraint(equalToConstant: EditorPanelMetrics.thumbnailHeight),
|
|
257
|
+
image.leadingAnchor.constraint(equalTo: frameView.leadingAnchor, constant: 2),
|
|
258
|
+
image.trailingAnchor.constraint(equalTo: frameView.trailingAnchor, constant: -2),
|
|
259
|
+
image.topAnchor.constraint(equalTo: frameView.topAnchor, constant: 2),
|
|
260
|
+
image.bottomAnchor.constraint(equalTo: frameView.bottomAnchor, constant: -2),
|
|
261
|
+
])
|
|
262
|
+
|
|
263
|
+
label.text = labelText
|
|
264
|
+
label.font = .systemFont(ofSize: 11, weight: .medium)
|
|
265
|
+
label.textColor = DesignTokens.textSecondary
|
|
266
|
+
label.textAlignment = .center
|
|
267
|
+
label.numberOfLines = 1
|
|
268
|
+
label.adjustsFontSizeToFitWidth = true
|
|
269
|
+
label.minimumScaleFactor = 0.8
|
|
270
|
+
label.isUserInteractionEnabled = false
|
|
271
|
+
|
|
272
|
+
let column = UIStackView(arrangedSubviews: [frameView, label])
|
|
273
|
+
column.axis = .vertical
|
|
274
|
+
column.alignment = .center
|
|
275
|
+
column.spacing = DesignTokens.spaceXs
|
|
276
|
+
column.isUserInteractionEnabled = false
|
|
277
|
+
|
|
278
|
+
addSubview(column)
|
|
279
|
+
column.translatesAutoresizingMaskIntoConstraints = false
|
|
280
|
+
NSLayoutConstraint.activate([
|
|
281
|
+
widthAnchor.constraint(equalToConstant: EditorPanelMetrics.thumbnailWidth + 12),
|
|
282
|
+
column.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
283
|
+
column.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
284
|
+
label.widthAnchor.constraint(equalTo: widthAnchor),
|
|
285
|
+
])
|
|
286
|
+
|
|
287
|
+
addAction(UIAction { [weak self] _ in self?.action?() }, for: .touchUpInside)
|
|
288
|
+
applyPressScale()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
292
|
+
return bounds.insetBy(dx: -8, dy: -8).contains(point)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
func setSelected(_ selected: Bool, accentColor: UIColor) {
|
|
296
|
+
frameView.layer.borderColor = (selected ? accentColor : UIColor.clear).cgColor
|
|
297
|
+
frameView.layer.borderWidth = 2
|
|
298
|
+
label.textColor = selected ? accentColor : DesignTokens.textSecondary
|
|
299
|
+
accessibilityTraits = selected ? [.button, .selected] : .button
|
|
300
|
+
}
|
|
174
301
|
}
|
|
175
302
|
|
|
176
303
|
/// Filter thumbnail: a preview of the current photo, outlined in purple when selected.
|
|
177
|
-
/// A thin outline rather than a filled container, so the photo stays readable.
|
|
178
304
|
final class EditorFilterThumbnail {
|
|
179
|
-
let
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
self.root = root
|
|
186
|
-
self.image = image
|
|
187
|
-
self.frame = frame
|
|
188
|
-
self.label = label
|
|
305
|
+
let control: EditorFilterThumbnailControl
|
|
306
|
+
var root: UIView { control }
|
|
307
|
+
var image: UIImageView { control.image }
|
|
308
|
+
|
|
309
|
+
init(control: EditorFilterThumbnailControl) {
|
|
310
|
+
self.control = control
|
|
189
311
|
}
|
|
190
312
|
|
|
191
313
|
func setSelected(_ selected: Bool, accentColor: UIColor) {
|
|
192
|
-
|
|
193
|
-
frame.layer.borderWidth = 2
|
|
194
|
-
label.textColor = selected ? accentColor : DesignTokens.textSecondary
|
|
314
|
+
control.setSelected(selected, accentColor: accentColor)
|
|
195
315
|
}
|
|
196
316
|
}
|
|
197
317
|
|
|
198
318
|
func editorFilterThumbnail(labelText: String, action: @escaping () -> Void) -> EditorFilterThumbnail {
|
|
199
|
-
let
|
|
200
|
-
|
|
201
|
-
image.clipsToBounds = true
|
|
202
|
-
image.layer.cornerRadius = DesignTokens.radiusMd
|
|
203
|
-
image.backgroundColor = DesignTokens.surfaceContainerHigh
|
|
204
|
-
|
|
205
|
-
// The outline lives on a frame *around* the image so the stroke never crops the preview.
|
|
206
|
-
let frame = UIView()
|
|
207
|
-
frame.layer.cornerRadius = DesignTokens.radiusMd + 2
|
|
208
|
-
frame.addSubview(image)
|
|
209
|
-
image.translatesAutoresizingMaskIntoConstraints = false
|
|
210
|
-
NSLayoutConstraint.activate([
|
|
211
|
-
image.widthAnchor.constraint(equalToConstant: EditorPanelMetrics.thumbnailWidth),
|
|
212
|
-
image.heightAnchor.constraint(equalToConstant: EditorPanelMetrics.thumbnailHeight),
|
|
213
|
-
image.leadingAnchor.constraint(equalTo: frame.leadingAnchor, constant: 2),
|
|
214
|
-
image.trailingAnchor.constraint(equalTo: frame.trailingAnchor, constant: -2),
|
|
215
|
-
image.topAnchor.constraint(equalTo: frame.topAnchor, constant: 2),
|
|
216
|
-
image.bottomAnchor.constraint(equalTo: frame.bottomAnchor, constant: -2),
|
|
217
|
-
])
|
|
218
|
-
|
|
219
|
-
let label = UILabel()
|
|
220
|
-
label.text = labelText
|
|
221
|
-
label.font = .systemFont(ofSize: 11, weight: .medium)
|
|
222
|
-
label.textColor = DesignTokens.textSecondary
|
|
223
|
-
label.textAlignment = .center
|
|
224
|
-
label.numberOfLines = 1
|
|
225
|
-
label.adjustsFontSizeToFitWidth = true
|
|
226
|
-
label.minimumScaleFactor = 0.8
|
|
227
|
-
|
|
228
|
-
let column = UIStackView(arrangedSubviews: [frame, label])
|
|
229
|
-
column.axis = .vertical
|
|
230
|
-
column.alignment = .center
|
|
231
|
-
column.spacing = DesignTokens.spaceXs
|
|
232
|
-
|
|
233
|
-
let root = UIView()
|
|
234
|
-
root.addSubview(column)
|
|
235
|
-
column.translatesAutoresizingMaskIntoConstraints = false
|
|
236
|
-
NSLayoutConstraint.activate([
|
|
237
|
-
root.widthAnchor.constraint(equalToConstant: EditorPanelMetrics.thumbnailWidth + 12),
|
|
238
|
-
column.centerXAnchor.constraint(equalTo: root.centerXAnchor),
|
|
239
|
-
column.centerYAnchor.constraint(equalTo: root.centerYAnchor),
|
|
240
|
-
label.widthAnchor.constraint(equalTo: root.widthAnchor),
|
|
241
|
-
])
|
|
242
|
-
root.isAccessibilityElement = true
|
|
243
|
-
root.accessibilityLabel = labelText
|
|
244
|
-
root.accessibilityTraits = .button
|
|
245
|
-
root.addGestureRecognizer(ClosureTapGestureRecognizer(action: action))
|
|
246
|
-
root.applyPressScale()
|
|
247
|
-
return EditorFilterThumbnail(root: root, image: image, frame: frame, label: label)
|
|
319
|
+
let control = EditorFilterThumbnailControl(labelText: labelText, action: action)
|
|
320
|
+
return EditorFilterThumbnail(control: control)
|
|
248
321
|
}
|
|
249
322
|
|
|
250
323
|
/// The editor's one slider: a `Label ......... value` line above a thin track, so a panel never
|
|
@@ -7,6 +7,8 @@ final class EditorToolSlider: UISlider {
|
|
|
7
7
|
override func trackRect(forBounds bounds: CGRect) -> CGRect {
|
|
8
8
|
var rect = super.trackRect(forBounds: bounds)
|
|
9
9
|
rect.origin.y = bounds.height * 0.65
|
|
10
|
+
rect.origin.x = bounds.origin.x + 16
|
|
11
|
+
rect.size.width = max(0, bounds.width - 32)
|
|
10
12
|
return rect
|
|
11
13
|
}
|
|
12
14
|
override func draw(_ rect: CGRect) {
|
|
@@ -15,8 +17,8 @@ final class EditorToolSlider: UISlider {
|
|
|
15
17
|
let suffix = propertyKey == "rotation" ? "°" : (propertyKey == "fontSize" ? "" : "%")
|
|
16
18
|
let valueText = "\(Int(value.rounded()))\(suffix)"
|
|
17
19
|
let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.monospacedDigitSystemFont(ofSize: 12, weight: .medium), .foregroundColor: DesignTokens.textSecondary]
|
|
18
|
-
(title as NSString).draw(at: CGPoint(x:
|
|
20
|
+
(title as NSString).draw(at: CGPoint(x: 16, y: 8), withAttributes: attributes)
|
|
19
21
|
let size = (valueText as NSString).size(withAttributes: attributes)
|
|
20
|
-
(valueText as NSString).draw(at: CGPoint(x: bounds.width - size.width -
|
|
22
|
+
(valueText as NSString).draw(at: CGPoint(x: bounds.width - size.width - 16, y: 8), withAttributes: attributes)
|
|
21
23
|
}
|
|
22
24
|
}
|
|
@@ -5,13 +5,13 @@ final class TextEditorSheet: UIViewController, UITextViewDelegate {
|
|
|
5
5
|
private let input = UITextView()
|
|
6
6
|
private let placeholder = UILabel()
|
|
7
7
|
private let initialText: String
|
|
8
|
-
private let
|
|
8
|
+
private let isEditingExisting: Bool
|
|
9
9
|
private let accent: UIColor
|
|
10
10
|
private let onSave: (String) -> Void
|
|
11
11
|
private var inputHeight: NSLayoutConstraint?
|
|
12
12
|
|
|
13
13
|
init(text: String, editing: Bool, accent: UIColor, onSave: @escaping (String) -> Void) {
|
|
14
|
-
initialText = text;
|
|
14
|
+
initialText = text; isEditingExisting = editing; self.accent = accent; self.onSave = onSave
|
|
15
15
|
super.init(nibName: nil, bundle: nil)
|
|
16
16
|
modalPresentationStyle = .overFullScreen
|
|
17
17
|
modalTransitionStyle = .crossDissolve
|
|
@@ -26,7 +26,7 @@ final class TextEditorSheet: UIViewController, UITextViewDelegate {
|
|
|
26
26
|
panel.layer.cornerRadius = 16
|
|
27
27
|
panel.isLayoutMarginsRelativeArrangement = true
|
|
28
28
|
panel.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
|
|
29
|
-
let title = UILabel(); title.text =
|
|
29
|
+
let title = UILabel(); title.text = isEditingExisting ? "Edit Text" : "Add Text"
|
|
30
30
|
title.textColor = .white; title.font = .systemFont(ofSize: 20, weight: .semibold)
|
|
31
31
|
panel.addArrangedSubview(title)
|
|
32
32
|
input.text = initialText; input.textColor = .white; input.tintColor = accent
|
|
@@ -50,7 +50,7 @@ final class TextEditorSheet: UIViewController, UITextViewDelegate {
|
|
|
50
50
|
let cancel = UIButton(type: .system); cancel.setTitle("Cancel", for: .normal); cancel.tintColor = .white
|
|
51
51
|
cancel.addAction(UIAction { [weak self] _ in self?.close() }, for: .touchUpInside)
|
|
52
52
|
let save = UIButton(type: .system)
|
|
53
|
-
save.setTitle(
|
|
53
|
+
save.setTitle(isEditingExisting ? "Done" : "Add", for: .normal); save.tintColor = .white
|
|
54
54
|
save.backgroundColor = accent; save.layer.cornerRadius = 12
|
|
55
55
|
save.addAction(UIAction { [weak self] _ in
|
|
56
56
|
guard let self, !self.input.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|