@onekeyfe/react-native-native-list 3.0.114 → 3.0.116
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/README.md +57 -3
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +47 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +795 -30
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +445 -91
- package/ios/NativeListCell.swift +655 -20
- package/ios/NativeListDesignAssets.swift +8 -0
- package/ios/RNCNativeListView.swift +275 -42
- package/ios/Resources/NativeListIcons.xcassets/onekey_badge_verified_solid.imageset/Contents.json +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_badge_verified_solid.imageset/icon.svg +1 -0
- package/lib/module/validation.js +145 -1
- package/lib/module/web/NativeListWebEngine.js +461 -51
- package/lib/typescript/src/models.d.ts +108 -2
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +37 -3
- package/package.json +4 -2
- package/src/models.ts +144 -3
- package/src/validation.ts +250 -0
- package/src/web/NativeListWebEngine.ts +766 -91
package/ios/NativeListCell.swift
CHANGED
|
@@ -4,6 +4,73 @@ import CoreText
|
|
|
4
4
|
import OneKeyImage
|
|
5
5
|
import UIKit
|
|
6
6
|
|
|
7
|
+
// Market/TokenListSkeleton: source geometry and the native Skeleton's 3s shimmer.
|
|
8
|
+
private final class NativeListMarketSkeleton: UIView {
|
|
9
|
+
private let marks = (0..<5).map { _ in UIView() }
|
|
10
|
+
private let gradients = (0..<5).map { _ in CAGradientLayer() }
|
|
11
|
+
|
|
12
|
+
init(background: UIColor) {
|
|
13
|
+
super.init(frame: .zero)
|
|
14
|
+
var white: CGFloat = 1
|
|
15
|
+
background.getWhite(&white, alpha: nil)
|
|
16
|
+
let base = UIColor(nativeListHex: white < 0.5 ? "#111111" : "#FAFAFA", fallback: .white)
|
|
17
|
+
let highlight = UIColor(nativeListHex: white < 0.5 ? "#333333" : "#CDCDCD", fallback: .lightGray)
|
|
18
|
+
for (index, mark) in marks.enumerated() {
|
|
19
|
+
mark.backgroundColor = base
|
|
20
|
+
mark.clipsToBounds = true
|
|
21
|
+
mark.layer.cornerRadius = index == 0 ? 16 : 8
|
|
22
|
+
let gradient = gradients[index]
|
|
23
|
+
gradient.cornerRadius = mark.layer.cornerRadius
|
|
24
|
+
gradient.colors = [base.cgColor, highlight.cgColor, base.cgColor]
|
|
25
|
+
gradient.locations = [0, 0.5, 1]
|
|
26
|
+
gradient.startPoint = CGPoint(x: 0, y: 0.5)
|
|
27
|
+
gradient.endPoint = CGPoint(x: 1, y: 0.5)
|
|
28
|
+
mark.layer.addSublayer(gradient)
|
|
29
|
+
addSubview(mark)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
34
|
+
|
|
35
|
+
override func layoutSubviews() {
|
|
36
|
+
super.layoutSubviews()
|
|
37
|
+
let frames = [
|
|
38
|
+
CGRect(x: 0, y: 0, width: 32, height: 32),
|
|
39
|
+
CGRect(x: 44, y: 0, width: 80, height: 16),
|
|
40
|
+
CGRect(x: 44, y: 20, width: 60, height: 12),
|
|
41
|
+
CGRect(x: bounds.width - 168, y: 7, width: 80, height: 18),
|
|
42
|
+
CGRect(x: bounds.width - 80, y: 7, width: 80, height: 18),
|
|
43
|
+
]
|
|
44
|
+
CATransaction.begin()
|
|
45
|
+
CATransaction.setDisableActions(true)
|
|
46
|
+
for (index, frame) in frames.enumerated() {
|
|
47
|
+
marks[index].frame = frame
|
|
48
|
+
gradients[index].frame = marks[index].bounds
|
|
49
|
+
}
|
|
50
|
+
CATransaction.commit()
|
|
51
|
+
updateAnimation()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override func didMoveToWindow() {
|
|
55
|
+
super.didMoveToWindow()
|
|
56
|
+
updateAnimation()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private func updateAnimation() {
|
|
60
|
+
for gradient in gradients {
|
|
61
|
+
guard window != nil else { gradient.removeAllAnimations(); continue }
|
|
62
|
+
guard gradient.bounds.width > 0, gradient.animation(forKey: "shimmer") == nil else { continue }
|
|
63
|
+
let animation = CABasicAnimation(keyPath: "transform.translation.x")
|
|
64
|
+
animation.fromValue = -gradient.bounds.width
|
|
65
|
+
animation.toValue = gradient.bounds.width
|
|
66
|
+
animation.duration = 3
|
|
67
|
+
animation.repeatCount = .infinity
|
|
68
|
+
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
|
69
|
+
gradient.add(animation, forKey: "shimmer")
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
7
74
|
final class NativeListActionOrigin {
|
|
8
75
|
weak var sourceView: UIView?
|
|
9
76
|
weak var ownerCell: NativeListCell?
|
|
@@ -12,6 +79,7 @@ final class NativeListActionOrigin {
|
|
|
12
79
|
let slot: Int?
|
|
13
80
|
// OneKey patch: expose the layout slot while preserving the larger hit target.
|
|
14
81
|
let anchorInset: CGFloat
|
|
82
|
+
var windowPoint: CGPoint?
|
|
15
83
|
|
|
16
84
|
init(
|
|
17
85
|
sourceView: UIView,
|
|
@@ -36,13 +104,17 @@ private final class NativeListAccessoryButton: UIButton {
|
|
|
36
104
|
didSet { invalidateIntrinsicContentSize(); setNeedsLayout() }
|
|
37
105
|
}
|
|
38
106
|
|
|
107
|
+
var marketLineHeight: CGFloat? {
|
|
108
|
+
didSet { invalidateIntrinsicContentSize(); setNeedsLayout() }
|
|
109
|
+
}
|
|
110
|
+
|
|
39
111
|
private var sourcePixelScale: CGFloat {
|
|
40
112
|
max(1, window?.screen.scale ?? traitCollection.displayScale)
|
|
41
113
|
}
|
|
42
114
|
|
|
43
115
|
override var intrinsicContentSize: CGSize {
|
|
44
116
|
var size = super.intrinsicContentSize
|
|
45
|
-
guard selectorSummaryLineHeight != nil, let title = attributedTitle(for: .normal) else { return size }
|
|
117
|
+
guard selectorSummaryLineHeight != nil || marketLineHeight != nil, let title = attributedTitle(for: .normal) else { return size }
|
|
46
118
|
let width = title.boundingRect(
|
|
47
119
|
with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
|
|
48
120
|
options: [.usesLineFragmentOrigin, .usesFontLeading],
|
|
@@ -54,6 +126,21 @@ private final class NativeListAccessoryButton: UIButton {
|
|
|
54
126
|
|
|
55
127
|
override func layoutSubviews() {
|
|
56
128
|
super.layoutSubviews()
|
|
129
|
+
if let lineHeight = marketLineHeight, let titleLabel, let title = attributedTitle(for: .normal) {
|
|
130
|
+
let measured = title.boundingRect(
|
|
131
|
+
with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
|
|
132
|
+
options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil
|
|
133
|
+
).width
|
|
134
|
+
let width = min(bounds.width, ceil(measured * sourcePixelScale) / sourcePixelScale)
|
|
135
|
+
let x = contentHorizontalAlignment == .trailing ? bounds.width - width
|
|
136
|
+
: contentHorizontalAlignment == .leading ? 0 : (bounds.width - width) / 2
|
|
137
|
+
titleLabel.frame = CGRect(
|
|
138
|
+
x: floor(x * sourcePixelScale) / sourcePixelScale,
|
|
139
|
+
y: (bounds.height - lineHeight) / 2,
|
|
140
|
+
width: width, height: lineHeight
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
57
144
|
guard let lineHeight = selectorSummaryLineHeight, let titleLabel else { return }
|
|
58
145
|
// OneKey patch: position the final source line box after UIKit has measured the button.
|
|
59
146
|
let top = ceil((bounds.height - lineHeight) / 2 * sourcePixelScale) / sourcePixelScale
|
|
@@ -357,11 +444,18 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
357
444
|
private let titleRowStack = UIStackView()
|
|
358
445
|
private let titleLabel = NativeListDottedUnderlineLabel()
|
|
359
446
|
private let subtitleLabel = UILabel()
|
|
447
|
+
// OneKey patch: keep Market name and volume in independent line boxes.
|
|
448
|
+
private let marketSubtitleStack = UIStackView()
|
|
449
|
+
private let marketSubtitleSpacer = UIView()
|
|
360
450
|
private let tertiaryLabel = UILabel()
|
|
361
451
|
private let statusLabel = NativeListInsetLabel()
|
|
362
452
|
private let metricSubtitleLabel = UILabel()
|
|
363
453
|
private let metricCompositeStack = UIStackView()
|
|
364
454
|
private let badgeLabel = NativeListInsetLabel()
|
|
455
|
+
// OneKey patch: reuse the existing explicit line-box layout for styled Market badges.
|
|
456
|
+
// private let marketBadgeButtons = (0..<3).map { _ in UIButton(type: .system) }
|
|
457
|
+
private let marketBadgeButtons = (0..<3).map { _ in NativeListAccessoryButton(type: .system) }
|
|
458
|
+
private let marketBadgeImages = (0..<3).map { _ in OneKeyImageReusableView(frame: .zero) }
|
|
365
459
|
private let actionStack = UIStackView()
|
|
366
460
|
private let actionButtons = (0..<3).map { _ in UIButton(type: .system) }
|
|
367
461
|
private let trailingStack = UIStackView()
|
|
@@ -412,6 +506,7 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
412
506
|
private var boundCheckboxData: [String: Any]?
|
|
413
507
|
private var boundCheckboxTarget: NativeSelectionTarget?
|
|
414
508
|
private var leadingActionKey: String?
|
|
509
|
+
private var marketBadgeActionKeys: [String?] = []
|
|
415
510
|
private var restingBackgroundColor: UIColor = .clear
|
|
416
511
|
private var pressedBackgroundColor = UIColor(
|
|
417
512
|
nativeListHex: "#E8E8E8",
|
|
@@ -612,6 +707,23 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
612
707
|
titleRowStack.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
613
708
|
titleRowStack.addArrangedSubview(titleLabel)
|
|
614
709
|
titleRowStack.addArrangedSubview(badgeLabel)
|
|
710
|
+
marketBadgeButtons.enumerated().forEach { index, button in
|
|
711
|
+
button.tag = index
|
|
712
|
+
button.addTarget(self, action: #selector(marketBadgePressed(_:)), for: .touchUpInside)
|
|
713
|
+
button.titleLabel?.font = nativeListFont(ofSize: 11, weight: .medium)
|
|
714
|
+
button.layer.cornerRadius = 4
|
|
715
|
+
button.clipsToBounds = true
|
|
716
|
+
let image = marketBadgeImages[index]
|
|
717
|
+
image.isUserInteractionEnabled = false
|
|
718
|
+
image.translatesAutoresizingMaskIntoConstraints = false
|
|
719
|
+
button.addSubview(image)
|
|
720
|
+
NSLayoutConstraint.activate([
|
|
721
|
+
image.centerYAnchor.constraint(equalTo: button.centerYAnchor),
|
|
722
|
+
image.widthAnchor.constraint(equalToConstant: 14),
|
|
723
|
+
image.heightAnchor.constraint(equalToConstant: 14),
|
|
724
|
+
])
|
|
725
|
+
titleRowStack.addArrangedSubview(button)
|
|
726
|
+
}
|
|
615
727
|
titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
616
728
|
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
617
729
|
badgeLabel.setContentHuggingPriority(.required, for: .horizontal)
|
|
@@ -691,6 +803,19 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
691
803
|
|
|
692
804
|
override func layoutSubviews() {
|
|
693
805
|
super.layoutSubviews()
|
|
806
|
+
if let item = currentItem, item.type == "system", item.data.string("presentation") == "market",
|
|
807
|
+
["noMatch", "retry"].contains(item.data.string("variant")), !titleLabel.isHidden {
|
|
808
|
+
// The cell content view owns the root constraints and must settle before reading descendants.
|
|
809
|
+
contentView.layoutIfNeeded()
|
|
810
|
+
// React Native floors text origins to physical pixels after centering the line box.
|
|
811
|
+
titleLabel.transform = .identity
|
|
812
|
+
let scale = max(1, window?.screen.scale ?? traitCollection.displayScale)
|
|
813
|
+
let origin = titleLabel.convert(titleLabel.bounds, to: contentView).origin
|
|
814
|
+
let x = floor((contentView.bounds.width - titleLabel.bounds.width) / 2 * scale) / scale
|
|
815
|
+
let contentHeight: CGFloat = item.data.string("variant") == "retry" ? 56 : 24
|
|
816
|
+
let y = floor(max(32, (contentView.bounds.height - contentHeight) / 2) * scale) / scale
|
|
817
|
+
titleLabel.transform = CGAffineTransform(translationX: x - origin.x, y: y - origin.y)
|
|
818
|
+
}
|
|
694
819
|
// OneKey patch: extend only the background across the section list outer inset.
|
|
695
820
|
if currentItem?.data.bool("backgroundFullWidth") == true {
|
|
696
821
|
selectorFullWidthBackground.frame = CGRect(x: -frame.minX, y: 0, width: superview?.bounds.width ?? bounds.width, height: bounds.height)
|
|
@@ -843,6 +968,7 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
843
968
|
case "activity": bindActivity(item, theme: theme)
|
|
844
969
|
case "message": bindMessage(item, theme: theme)
|
|
845
970
|
case "dataRow": bindDataRow(item, theme: theme, checkboxState)
|
|
971
|
+
case "market": bindMarket(item, theme: theme)
|
|
846
972
|
case "mediaTile": bindMediaTile(item, theme: theme)
|
|
847
973
|
case "metricCard": bindMetricCard(item, theme: theme)
|
|
848
974
|
case "sectionHeader": bindSectionHeader(item, theme: theme, layout: layout, checkboxState)
|
|
@@ -1033,6 +1159,7 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
1033
1159
|
}
|
|
1034
1160
|
|
|
1035
1161
|
private func reset() {
|
|
1162
|
+
titleLabel.transform = .identity
|
|
1036
1163
|
restoreSelectorTypography()
|
|
1037
1164
|
// OneKey patch: remove selector decorations before rebinding recycled cells.
|
|
1038
1165
|
selectorViews.forEach { $0.removeFromSuperview() }
|
|
@@ -1041,6 +1168,7 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
1041
1168
|
selectorConstraints.removeAll()
|
|
1042
1169
|
selectorImages.forEach { $0.prepareForReuse() }
|
|
1043
1170
|
selectorImages.removeAll()
|
|
1171
|
+
marketBadgeImages.forEach { $0.prepareForReuse(); $0.isHidden = true }
|
|
1044
1172
|
selectorFullWidthBackground.removeFromSuperlayer()
|
|
1045
1173
|
selectorBorder?.removeFromSuperlayer()
|
|
1046
1174
|
selectorBorder = nil
|
|
@@ -1094,6 +1222,15 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
1094
1222
|
headerValueIconImageView.removeFromSuperview()
|
|
1095
1223
|
headerValueIconImageView.image = nil
|
|
1096
1224
|
headerValueIconImageView.isHidden = true
|
|
1225
|
+
// OneKey patch: restore the shared labels before any recycled row binds.
|
|
1226
|
+
marketSubtitleStack.arrangedSubviews.forEach {
|
|
1227
|
+
marketSubtitleStack.removeArrangedSubview($0)
|
|
1228
|
+
$0.removeFromSuperview()
|
|
1229
|
+
}
|
|
1230
|
+
mainStack.removeArrangedSubview(marketSubtitleStack)
|
|
1231
|
+
marketSubtitleStack.removeFromSuperview()
|
|
1232
|
+
mainStack.removeArrangedSubview(tertiaryLabel)
|
|
1233
|
+
tertiaryLabel.removeFromSuperview()
|
|
1097
1234
|
mediaMetadataStack.removeArrangedSubview(subtitleLabel)
|
|
1098
1235
|
mediaMetadataStack.removeArrangedSubview(mediaNetworkImage)
|
|
1099
1236
|
mediaMetadataStack.removeFromSuperview()
|
|
@@ -1103,6 +1240,7 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
1103
1240
|
subtitleLabel.removeFromSuperview()
|
|
1104
1241
|
mainStack.insertArrangedSubview(titleRowStack, at: 0)
|
|
1105
1242
|
mainStack.insertArrangedSubview(subtitleLabel, at: 1)
|
|
1243
|
+
mainStack.insertArrangedSubview(tertiaryLabel, at: 2)
|
|
1106
1244
|
leadingWidth.constant = 40
|
|
1107
1245
|
leadingHeight.constant = 40
|
|
1108
1246
|
leadingIconWidth.constant = 18
|
|
@@ -1123,6 +1261,7 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
1123
1261
|
$0.isHidden = true
|
|
1124
1262
|
$0.alpha = 1
|
|
1125
1263
|
$0.layer.cornerRadius = 0
|
|
1264
|
+
$0.layer.mask = nil
|
|
1126
1265
|
}
|
|
1127
1266
|
leadingContainer.clipsToBounds = true
|
|
1128
1267
|
leadingContainer.layer.borderWidth = 0
|
|
@@ -1156,6 +1295,13 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
1156
1295
|
titleLabel.textAlignment = .natural
|
|
1157
1296
|
subtitleLabel.text = nil
|
|
1158
1297
|
subtitleLabel.lineBreakMode = .byTruncatingTail
|
|
1298
|
+
subtitleLabel.attributedText = nil
|
|
1299
|
+
subtitleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
1300
|
+
subtitleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
1301
|
+
tertiaryLabel.attributedText = nil
|
|
1302
|
+
tertiaryLabel.lineBreakMode = .byTruncatingTail
|
|
1303
|
+
tertiaryLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
1304
|
+
tertiaryLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
1159
1305
|
tertiaryLabel.text = nil
|
|
1160
1306
|
statusLabel.text = nil
|
|
1161
1307
|
statusLabel.topInset = 0
|
|
@@ -1178,6 +1324,28 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
1178
1324
|
badgeLabel.backgroundColor = .clear
|
|
1179
1325
|
badgeLabel.layer.cornerRadius = 0
|
|
1180
1326
|
badgeLabel.clipsToBounds = false
|
|
1327
|
+
marketBadgeButtons.forEach {
|
|
1328
|
+
$0.isHidden = true
|
|
1329
|
+
$0.isEnabled = true
|
|
1330
|
+
$0.isUserInteractionEnabled = false
|
|
1331
|
+
// OneKey patch: a recycled badge must not retain an attributed title or font.
|
|
1332
|
+
$0.setAttributedTitle(nil, for: .normal)
|
|
1333
|
+
$0.marketLineHeight = nil
|
|
1334
|
+
$0.selectorSummaryLineHeight = nil
|
|
1335
|
+
$0.titleLabel?.font = nativeListFont(ofSize: 11, weight: .medium)
|
|
1336
|
+
$0.titleLabel?.numberOfLines = 1
|
|
1337
|
+
$0.contentHorizontalAlignment = .center
|
|
1338
|
+
$0.accessibilityLabel = nil
|
|
1339
|
+
$0.accessibilityTraits = .staticText
|
|
1340
|
+
$0.setTitle(nil, for: .normal)
|
|
1341
|
+
$0.setImage(nil, for: .normal)
|
|
1342
|
+
$0.setTitleColor(nil, for: .normal)
|
|
1343
|
+
$0.tintColor = nil
|
|
1344
|
+
$0.backgroundColor = .clear
|
|
1345
|
+
$0.contentEdgeInsets = .zero
|
|
1346
|
+
$0.imageEdgeInsets = .zero
|
|
1347
|
+
$0.titleEdgeInsets = .zero
|
|
1348
|
+
}
|
|
1181
1349
|
[titleLabel, subtitleLabel, tertiaryLabel, statusLabel, metricSubtitleLabel, badgeLabel, actionStack]
|
|
1182
1350
|
.forEach { $0.isHidden = true }
|
|
1183
1351
|
separatorView.isHidden = true
|
|
@@ -1188,7 +1356,9 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
1188
1356
|
$0.backgroundColor = .clear
|
|
1189
1357
|
}
|
|
1190
1358
|
accessoryButtons.enumerated().forEach { index, button in
|
|
1359
|
+
button.isUserInteractionEnabled = true
|
|
1191
1360
|
button.selectorSummaryLineHeight = nil
|
|
1361
|
+
button.marketLineHeight = nil
|
|
1192
1362
|
button.titleLabel?.font = nativeListFont(
|
|
1193
1363
|
ofSize: index == 0 ? 16 : 14,
|
|
1194
1364
|
weight: index == 0 ? .medium : .regular
|
|
@@ -1233,6 +1403,7 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
1233
1403
|
boundCheckboxData = nil
|
|
1234
1404
|
boundCheckboxTarget = nil
|
|
1235
1405
|
leadingActionKey = nil
|
|
1406
|
+
marketBadgeActionKeys = []
|
|
1236
1407
|
layer.maskedCorners = []
|
|
1237
1408
|
layer.cornerRadius = 0
|
|
1238
1409
|
layer.cornerCurve = .circular
|
|
@@ -2014,6 +2185,347 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
2014
2185
|
}
|
|
2015
2186
|
}
|
|
2016
2187
|
|
|
2188
|
+
private func marketFontWeight(_ value: String, fallback: NativeListFontWeight) -> NativeListFontWeight {
|
|
2189
|
+
switch value {
|
|
2190
|
+
case "regular": return .regular
|
|
2191
|
+
case "semibold": return .semibold
|
|
2192
|
+
case "bold": return .bold
|
|
2193
|
+
case "medium": return .medium
|
|
2194
|
+
default: return fallback
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
private func marketTextAlignment(_ value: String) -> NSTextAlignment {
|
|
2199
|
+
switch value {
|
|
2200
|
+
case "center": return .center
|
|
2201
|
+
case "start": return effectiveUserInterfaceLayoutDirection == .rightToLeft ? .right : .left
|
|
2202
|
+
case "end": return effectiveUserInterfaceLayoutDirection == .rightToLeft ? .left : .right
|
|
2203
|
+
default: return .natural
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
private func applyMarketTextStyle(
|
|
2208
|
+
_ label: UILabel,
|
|
2209
|
+
data: [String: Any]?,
|
|
2210
|
+
theme: [String: Any]?,
|
|
2211
|
+
defaultSize: CGFloat,
|
|
2212
|
+
defaultLineHeight: CGFloat,
|
|
2213
|
+
defaultWeight: NativeListFontWeight,
|
|
2214
|
+
defaultColor: UIColor,
|
|
2215
|
+
defaultAlignment: NSTextAlignment = .natural
|
|
2216
|
+
) {
|
|
2217
|
+
let size = CGFloat(data?.double("fontSize", default: Double(defaultSize)) ?? Double(defaultSize))
|
|
2218
|
+
let lineHeight = CGFloat(data?.double("lineHeight", default: Double(defaultLineHeight)) ?? Double(defaultLineHeight))
|
|
2219
|
+
label.font = nativeListTabularFont(ofSize: size, weight: marketFontWeight(data?.string("fontWeight") ?? "", fallback: defaultWeight))
|
|
2220
|
+
label.textColor = data?["color"].flatMap { $0 as? String }.map { UIColor(nativeListHex: $0, fallback: defaultColor) } ?? defaultColor
|
|
2221
|
+
label.textAlignment = data?["alignment"] == nil
|
|
2222
|
+
? defaultAlignment
|
|
2223
|
+
: marketTextAlignment(data?.string("alignment") ?? "")
|
|
2224
|
+
label.numberOfLines = min(2, max(1, data?.int("lines", default: 1) ?? 1))
|
|
2225
|
+
setLineHeight(label, text: label.text ?? "", lineHeight: lineHeight)
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
private func applyMarketButtonStyle(
|
|
2229
|
+
_ button: UIButton,
|
|
2230
|
+
data: [String: Any]?,
|
|
2231
|
+
defaultSize: CGFloat,
|
|
2232
|
+
defaultLineHeight: CGFloat,
|
|
2233
|
+
defaultWeight: NativeListFontWeight,
|
|
2234
|
+
color: UIColor,
|
|
2235
|
+
defaultAlignment: UIControl.ContentHorizontalAlignment
|
|
2236
|
+
) {
|
|
2237
|
+
let size = CGFloat(data?.double("fontSize", default: Double(defaultSize)) ?? Double(defaultSize))
|
|
2238
|
+
let lineHeight = CGFloat(data?.double("lineHeight", default: Double(defaultLineHeight)) ?? Double(defaultLineHeight))
|
|
2239
|
+
setButtonLine(
|
|
2240
|
+
button,
|
|
2241
|
+
text: button.title(for: .normal) ?? "",
|
|
2242
|
+
font: nativeListTabularFont(ofSize: size, weight: marketFontWeight(data?.string("fontWeight") ?? "", fallback: defaultWeight)),
|
|
2243
|
+
color: data?["color"].flatMap { $0 as? String }.map { UIColor(nativeListHex: $0, fallback: color) } ?? color,
|
|
2244
|
+
lineHeight: lineHeight
|
|
2245
|
+
)
|
|
2246
|
+
if data?["alignment"] == nil {
|
|
2247
|
+
button.contentHorizontalAlignment = defaultAlignment
|
|
2248
|
+
} else {
|
|
2249
|
+
button.contentHorizontalAlignment = data?.string("alignment") == "start"
|
|
2250
|
+
? .leading
|
|
2251
|
+
: data?.string("alignment") == "end" ? .trailing : .center
|
|
2252
|
+
}
|
|
2253
|
+
button.titleLabel?.numberOfLines = min(2, max(1, data?.int("lines", default: 1) ?? 1))
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
private func marketAttributedText(
|
|
2257
|
+
_ text: String,
|
|
2258
|
+
segments: [[String: Any]],
|
|
2259
|
+
style: [String: Any]?,
|
|
2260
|
+
defaultSize: CGFloat,
|
|
2261
|
+
defaultLineHeight: CGFloat,
|
|
2262
|
+
defaultWeight: NativeListFontWeight,
|
|
2263
|
+
color: UIColor,
|
|
2264
|
+
defaultAlignment: NSTextAlignment
|
|
2265
|
+
) -> NSAttributedString {
|
|
2266
|
+
let size = CGFloat(style?.double("fontSize", default: Double(defaultSize)) ?? Double(defaultSize))
|
|
2267
|
+
let lineHeight = CGFloat(style?.double("lineHeight", default: Double(defaultLineHeight)) ?? Double(defaultLineHeight))
|
|
2268
|
+
let paragraph = NSMutableParagraphStyle()
|
|
2269
|
+
paragraph.minimumLineHeight = lineHeight
|
|
2270
|
+
paragraph.maximumLineHeight = lineHeight
|
|
2271
|
+
paragraph.alignment = style?["alignment"] == nil
|
|
2272
|
+
? defaultAlignment
|
|
2273
|
+
: marketTextAlignment(style?.string("alignment") ?? "")
|
|
2274
|
+
let weight = marketFontWeight(style?.string("fontWeight") ?? "", fallback: defaultWeight)
|
|
2275
|
+
let resolvedColor = style?["color"].flatMap { $0 as? String }.map { UIColor(nativeListHex: $0, fallback: color) } ?? color
|
|
2276
|
+
let result = NSMutableAttributedString(string: "")
|
|
2277
|
+
let source = segments.isEmpty ? [["text": text]] : segments
|
|
2278
|
+
for segment in source {
|
|
2279
|
+
let segmentSize = segment.string("style") == "subscript" ? ceil(size * 0.6) : size
|
|
2280
|
+
result.append(NSAttributedString(string: segment.string("text"), attributes: [
|
|
2281
|
+
.font: nativeListTabularFont(ofSize: segmentSize, weight: weight),
|
|
2282
|
+
.foregroundColor: resolvedColor,
|
|
2283
|
+
.kern: 0,
|
|
2284
|
+
.paragraphStyle: paragraph,
|
|
2285
|
+
.baselineOffset: max(0, (lineHeight - nativeListTabularFont(ofSize: size, weight: weight).lineHeight) / 2),
|
|
2286
|
+
]))
|
|
2287
|
+
}
|
|
2288
|
+
return result
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
private func marketLeading(_ item: NativeListItem, style: [String: Any]?) -> [String: Any]? {
|
|
2292
|
+
guard var visual = item.data.dictionary("leading") else { return nil }
|
|
2293
|
+
guard let imageStyle = style?.dictionary("image") else { return visual }
|
|
2294
|
+
if let shape = imageStyle["shape"] as? String { visual["shape"] = shape }
|
|
2295
|
+
if let contentFit = imageStyle["contentFit"] as? String, var image = visual.dictionary("image") {
|
|
2296
|
+
image["contentFit"] = contentFit
|
|
2297
|
+
visual["image"] = image
|
|
2298
|
+
}
|
|
2299
|
+
return visual
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
private func bindMarket(_ item: NativeListItem, theme: [String: Any]?) {
|
|
2303
|
+
let variant = item.data.string("variant")
|
|
2304
|
+
let style = item.data.dictionary("style")
|
|
2305
|
+
let imageStyle = style?.dictionary("image")
|
|
2306
|
+
let imageWidth = CGFloat(imageStyle?.double("width", default: variant == "stock" ? 40 : 32) ?? (variant == "stock" ? 40 : 32))
|
|
2307
|
+
let imageHeight = CGFloat(imageStyle?.double("height", default: variant == "stock" ? 40 : 32) ?? (variant == "stock" ? 40 : 32))
|
|
2308
|
+
let horizontalPadding = CGFloat(style?.double("horizontalPadding", default: variant == "perp" ? 16 : 20) ?? (variant == "perp" ? 16 : 20))
|
|
2309
|
+
let verticalPadding = CGFloat(style?.double("verticalPadding", default: 12) ?? 12)
|
|
2310
|
+
rootLeadingConstraint.constant = horizontalPadding
|
|
2311
|
+
rootTrailingConstraint.constant = -horizontalPadding
|
|
2312
|
+
rootTopConstraint.constant = verticalPadding
|
|
2313
|
+
rootBottomConstraint.constant = -verticalPadding
|
|
2314
|
+
rootStack.spacing = CGFloat(style?.double("leadingGap", default: variant == "perp" ? 8 : 14) ?? (variant == "perp" ? 8 : 14))
|
|
2315
|
+
leadingWidth.constant = imageWidth
|
|
2316
|
+
leadingHeight.constant = imageHeight
|
|
2317
|
+
if let visual = marketLeading(item, style: style) {
|
|
2318
|
+
addLeading(visual, key: item.key)
|
|
2319
|
+
let radius = CGFloat(imageStyle?.double("cornerRadius", default: imageStyle?.string("shape") == "square" ? 0 : imageStyle?.string("shape") == "rounded" ? 8 : Double(min(imageWidth, imageHeight) / 2)) ?? Double(min(imageWidth, imageHeight) / 2))
|
|
2320
|
+
leadingContainer.layer.cornerRadius = radius
|
|
2321
|
+
leadingImages.first?.layer.cornerRadius = radius
|
|
2322
|
+
if let image = leadingImages.first, !visual.string("borderColor").isEmpty {
|
|
2323
|
+
// Match Token's border box: the bitmap occupies the one-point inset.
|
|
2324
|
+
leadingContainer.layer.borderWidth = 1
|
|
2325
|
+
leadingContainer.layer.borderColor = UIColor(nativeListHex: visual.string("borderColor"), fallback: .clear).cgColor
|
|
2326
|
+
for constraint in leadingSlotConstraints where constraint.firstItem === image {
|
|
2327
|
+
switch constraint.firstAttribute {
|
|
2328
|
+
case .leading, .top: constraint.constant = 1
|
|
2329
|
+
case .trailing, .bottom: constraint.constant = -1
|
|
2330
|
+
default: break
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
image.layer.cornerRadius = 0
|
|
2334
|
+
let mask = CAShapeLayer()
|
|
2335
|
+
mask.path = UIBezierPath(roundedRect: CGRect(x: -1, y: -1, width: imageWidth, height: imageHeight), cornerRadius: radius).cgPath
|
|
2336
|
+
image.layer.mask = mask
|
|
2337
|
+
}
|
|
2338
|
+
if let diagnostic = item.data.dictionary("diagnostics")?.string("imageBindActionKey"), !diagnostic.isEmpty,
|
|
2339
|
+
visual.dictionary("image") != nil || visual.dictionary("networkImage") != nil {
|
|
2340
|
+
onAction?(item, diagnostic, nil, nil)
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
rootStack.addArrangedSubview(mainStack)
|
|
2344
|
+
// OneKey patch: opt in to the source Market row's content gap.
|
|
2345
|
+
// rootStack.setCustomSpacing(0, after: mainStack)
|
|
2346
|
+
rootStack.setCustomSpacing(CGFloat(style?.double("contentTrailingGap", default: 0) ?? 0), after: mainStack)
|
|
2347
|
+
mainStack.spacing = CGFloat(style?.double("lineGap", default: 0) ?? 0)
|
|
2348
|
+
titleRowStack.spacing = CGFloat(style?.double("titleBadgeGap", default: 4) ?? 4)
|
|
2349
|
+
// OneKey patch: opt in without changing the other row templates' filled layout.
|
|
2350
|
+
if style?.string("titleBadgeLayout") == "inline" {
|
|
2351
|
+
mainStack.alignment = .leading
|
|
2352
|
+
titleRowStack.setContentHuggingPriority(.required, for: .horizontal)
|
|
2353
|
+
}
|
|
2354
|
+
show(titleLabel, item.data.string("title"), lines: style?.dictionary("title")?.int("lines", default: 1) ?? 1)
|
|
2355
|
+
applyMarketTextStyle(titleLabel, data: style?.dictionary("title"), theme: theme, defaultSize: 16, defaultLineHeight: 24, defaultWeight: .medium, defaultColor: nativeListColor(theme, "primaryText", "#202020"))
|
|
2356
|
+
let badges = Array(item.data.dictionaries("badges").prefix(marketBadgeButtons.count))
|
|
2357
|
+
marketBadgeActionKeys = badges.map { $0["actionKey"] as? String }
|
|
2358
|
+
for (index, badge) in badges.enumerated() {
|
|
2359
|
+
let button = marketBadgeButtons[index]
|
|
2360
|
+
let badgeStyle = badge.dictionary("style")
|
|
2361
|
+
let badgeFontSize = CGFloat(badgeStyle?.double("fontSize", default: 11) ?? 11)
|
|
2362
|
+
let badgeFontWeight = marketFontWeight(badgeStyle?.string("fontWeight") ?? "", fallback: .medium)
|
|
2363
|
+
// OneKey patch: SizableText supplies tabular numerals for explicit Market metrics.
|
|
2364
|
+
let badgeFont = badgeStyle == nil
|
|
2365
|
+
? nativeListFont(ofSize: badgeFontSize, weight: badgeFontWeight)
|
|
2366
|
+
: nativeListTabularFont(ofSize: badgeFontSize, weight: badgeFontWeight)
|
|
2367
|
+
let hasBuiltInIcon = badge.string("iconName") == "verified"
|
|
2368
|
+
let hasRemoteIcon = badge.dictionary("icon") != nil
|
|
2369
|
+
let hasIcon = hasBuiltInIcon || hasRemoteIcon
|
|
2370
|
+
let text = badge.string("text")
|
|
2371
|
+
let toneColor: UIColor
|
|
2372
|
+
switch badge.string("tone") {
|
|
2373
|
+
case "success": toneColor = nativeListColor(theme, "positive", "#218358")
|
|
2374
|
+
case "danger": toneColor = nativeListColor(theme, "negative", "#CE2C31")
|
|
2375
|
+
case "info": toneColor = nativeListColor(theme, "info", "#0D74CE")
|
|
2376
|
+
case "warning": toneColor = nativeListColor(theme, "primaryText", "#202020")
|
|
2377
|
+
default: toneColor = nativeListColor(theme, "secondaryText", "#646464")
|
|
2378
|
+
}
|
|
2379
|
+
let foreground = UIColor(
|
|
2380
|
+
nativeListHex: badge.string("textColor", default: ""),
|
|
2381
|
+
fallback: toneColor
|
|
2382
|
+
)
|
|
2383
|
+
button.isHidden = false
|
|
2384
|
+
button.isEnabled = true
|
|
2385
|
+
button.isUserInteractionEnabled = !badge.string("actionKey").isEmpty
|
|
2386
|
+
button.accessibilityTraits = button.isUserInteractionEnabled ? .button : .staticText
|
|
2387
|
+
button.setTitle(text, for: .normal)
|
|
2388
|
+
button.setTitleColor(foreground, for: .normal)
|
|
2389
|
+
button.titleLabel?.font = badgeFont
|
|
2390
|
+
if let lineHeight = badgeStyle?["lineHeight"] as? Double {
|
|
2391
|
+
setButtonLine(button, text: text, font: badgeFont, color: foreground, lineHeight: CGFloat(lineHeight))
|
|
2392
|
+
// The explicit text-only line box must not cover an adjacent icon.
|
|
2393
|
+
if hasIcon { button.marketLineHeight = nil }
|
|
2394
|
+
}
|
|
2395
|
+
button.tintColor = foreground
|
|
2396
|
+
button.backgroundColor = UIColor(
|
|
2397
|
+
nativeListHex: badge.string("backgroundColor", default: ""),
|
|
2398
|
+
fallback: hasIcon && text.isEmpty
|
|
2399
|
+
? .clear
|
|
2400
|
+
: nativeListColor(theme, "strongBackground", "#0000000F")
|
|
2401
|
+
)
|
|
2402
|
+
let iconOnly = hasIcon && text.isEmpty
|
|
2403
|
+
let iconSize: CGFloat = hasBuiltInIcon ? 16 : 14
|
|
2404
|
+
// OneKey patch: preserve the native defaults unless the caller supplies padding.
|
|
2405
|
+
let padding = CGFloat(badgeStyle?.double("horizontalPadding", default: 5) ?? 5)
|
|
2406
|
+
let hasCustomPadding = badgeStyle?["horizontalPadding"] != nil
|
|
2407
|
+
let leftPadding = hasCustomPadding ? padding + (hasRemoteIcon ? iconSize + 2 : 0) : hasRemoteIcon ? 20 : hasIcon ? 3 : 5
|
|
2408
|
+
// button.contentEdgeInsets = UIEdgeInsets(top: 0, left: iconOnly ? 0 : hasRemoteIcon ? 20 : hasIcon ? 3 : 5, bottom: 0, right: iconOnly ? 0 : 5)
|
|
2409
|
+
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: iconOnly ? 0 : leftPadding, bottom: 0, right: iconOnly ? 0 : padding)
|
|
2410
|
+
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: text.isEmpty ? 0 : 3)
|
|
2411
|
+
button.titleEdgeInsets = .zero
|
|
2412
|
+
button.imageView?.contentMode = .scaleAspectFit
|
|
2413
|
+
if hasBuiltInIcon {
|
|
2414
|
+
button.setImage(
|
|
2415
|
+
nativeListIcon(named: "BadgeVerifiedSolid", size: CGSize(width: iconSize, height: iconSize)),
|
|
2416
|
+
for: .normal
|
|
2417
|
+
)
|
|
2418
|
+
}
|
|
2419
|
+
// OneKey patch: match source badge metrics at physical-pixel precision.
|
|
2420
|
+
// let height = button.heightAnchor.constraint(equalToConstant: 18)
|
|
2421
|
+
let height = button.heightAnchor.constraint(equalToConstant: CGFloat(badgeStyle?.double("height", default: 18) ?? 18))
|
|
2422
|
+
let textWidth = (text as NSString).size(withAttributes: [.font: badgeFont]).width
|
|
2423
|
+
let scale = max(1, traitCollection.displayScale)
|
|
2424
|
+
let roundedTextWidth = badgeStyle == nil ? ceil(textWidth) : ceil(textWidth * scale) / scale
|
|
2425
|
+
let extraWidth = hasCustomPadding ? padding * 2 + (hasIcon ? iconSize + 2 : 0) : hasIcon ? iconSize + 11 : 10
|
|
2426
|
+
// let width = button.widthAnchor.constraint(equalToConstant: iconOnly ? iconSize : ceil(textWidth) + (hasIcon ? iconSize + 11 : 10))
|
|
2427
|
+
let width = button.widthAnchor.constraint(equalToConstant: iconOnly ? iconSize : roundedTextWidth + extraWidth)
|
|
2428
|
+
NSLayoutConstraint.activate([width, height])
|
|
2429
|
+
selectorConstraints.append(contentsOf: [width, height])
|
|
2430
|
+
if let icon = badge.dictionary("icon") {
|
|
2431
|
+
marketBadgeImages[index].isHidden = false
|
|
2432
|
+
marketBadgeImages[index].layer.cornerRadius = 7
|
|
2433
|
+
marketBadgeImages[index].clipsToBounds = true
|
|
2434
|
+
let imageLeading = marketBadgeImages[index].leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: text.isEmpty ? 0 : 4)
|
|
2435
|
+
imageLeading.isActive = true
|
|
2436
|
+
selectorConstraints.append(imageLeading)
|
|
2437
|
+
bindImage(icon, into: marketBadgeImages[index], token: item.key, slot: 20 + index, variant: "generic")
|
|
2438
|
+
}
|
|
2439
|
+
button.accessibilityLabel = badge.string("accessibilityLabel", default: badge.string("text"))
|
|
2440
|
+
}
|
|
2441
|
+
if !item.data.string("subtitle").isEmpty || !item.data.dictionaries("subtitleSegments").isEmpty {
|
|
2442
|
+
show(subtitleLabel, item.data.string("subtitle"), lines: style?.dictionary("subtitle")?.int("lines", default: 1) ?? 1)
|
|
2443
|
+
applyMarketTextStyle(subtitleLabel, data: style?.dictionary("subtitle"), theme: theme, defaultSize: 14, defaultLineHeight: 20, defaultWeight: .regular, defaultColor: nativeListColor(theme, "secondaryText", "#646464"))
|
|
2444
|
+
if !item.data.dictionaries("subtitleSegments").isEmpty {
|
|
2445
|
+
subtitleLabel.attributedText = marketAttributedText(item.data.string("subtitle"), segments: item.data.dictionaries("subtitleSegments"), style: style?.dictionary("subtitle"), defaultSize: 14, defaultLineHeight: 20, defaultWeight: .regular, color: nativeListColor(theme, "secondaryText", "#646464"), defaultAlignment: .natural)
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
// OneKey patch: preserve volume width while the localized name truncates.
|
|
2449
|
+
let subtitlePrefix = item.data.dictionary("subtitlePrefix")
|
|
2450
|
+
let subtitlePadding = CGFloat(style?.double("subtitleTrailingPadding", default: 0) ?? 0)
|
|
2451
|
+
if subtitlePrefix != nil || subtitlePadding > 0 {
|
|
2452
|
+
mainStack.removeArrangedSubview(subtitleLabel)
|
|
2453
|
+
subtitleLabel.removeFromSuperview()
|
|
2454
|
+
mainStack.removeArrangedSubview(tertiaryLabel)
|
|
2455
|
+
tertiaryLabel.removeFromSuperview()
|
|
2456
|
+
marketSubtitleStack.axis = .horizontal
|
|
2457
|
+
marketSubtitleStack.alignment = .center
|
|
2458
|
+
marketSubtitleStack.spacing = 0
|
|
2459
|
+
marketSubtitleStack.clipsToBounds = true
|
|
2460
|
+
show(tertiaryLabel, subtitlePrefix?.string("text") ?? "", lines: 1)
|
|
2461
|
+
applyMarketTextStyle(tertiaryLabel, data: subtitlePrefix?.dictionary("style"), theme: theme, defaultSize: 12, defaultLineHeight: 16, defaultWeight: .regular, defaultColor: nativeListColor(theme, "secondaryText", "#646464"))
|
|
2462
|
+
tertiaryLabel.setContentHuggingPriority(.required, for: .horizontal)
|
|
2463
|
+
tertiaryLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
2464
|
+
subtitleLabel.setContentHuggingPriority(.required, for: .horizontal)
|
|
2465
|
+
subtitleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
2466
|
+
marketSubtitleStack.addArrangedSubview(tertiaryLabel)
|
|
2467
|
+
marketSubtitleStack.addArrangedSubview(subtitleLabel)
|
|
2468
|
+
marketSubtitleStack.addArrangedSubview(marketSubtitleSpacer)
|
|
2469
|
+
if !tertiaryLabel.isHidden && !subtitleLabel.isHidden {
|
|
2470
|
+
marketSubtitleStack.setCustomSpacing(CGFloat(subtitlePrefix?.double("gap", default: 4) ?? 4), after: tertiaryLabel)
|
|
2471
|
+
}
|
|
2472
|
+
mainStack.insertArrangedSubview(marketSubtitleStack, at: 1)
|
|
2473
|
+
let width = marketSubtitleStack.widthAnchor.constraint(equalTo: mainStack.widthAnchor, constant: -subtitlePadding)
|
|
2474
|
+
width.isActive = true
|
|
2475
|
+
selectorConstraints.append(width)
|
|
2476
|
+
if let maxWidth = subtitlePrefix?["maxWidth"] as? Double {
|
|
2477
|
+
let limit = tertiaryLabel.widthAnchor.constraint(lessThanOrEqualToConstant: CGFloat(maxWidth))
|
|
2478
|
+
limit.isActive = true
|
|
2479
|
+
selectorConstraints.append(limit)
|
|
2480
|
+
}
|
|
2481
|
+
marketSubtitleStack.isHidden = tertiaryLabel.isHidden && subtitleLabel.isHidden
|
|
2482
|
+
}
|
|
2483
|
+
rootStack.addArrangedSubview(trailingStack)
|
|
2484
|
+
trailingStack.axis = .horizontal
|
|
2485
|
+
trailingStack.alignment = .center
|
|
2486
|
+
trailingStack.spacing = CGFloat(style?.double("trailingGap", default: 8) ?? 8)
|
|
2487
|
+
updateMarketQuote(item, theme: theme)
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
func updateMarketQuote(_ item: NativeListItem, theme: [String: Any]?) {
|
|
2491
|
+
guard currentItem?.key == item.key, item.type == "market" else { return }
|
|
2492
|
+
currentItem = item
|
|
2493
|
+
NSLayoutConstraint.deactivate(accessorySizeConstraints)
|
|
2494
|
+
accessorySizeConstraints.removeAll()
|
|
2495
|
+
let style = item.data.dictionary("style")
|
|
2496
|
+
let price = accessoryButtons[0]
|
|
2497
|
+
price.isUserInteractionEnabled = false
|
|
2498
|
+
price.isHidden = false
|
|
2499
|
+
price.setAttributedTitle(nil, for: .normal)
|
|
2500
|
+
price.setTitle(item.data.string("price"), for: .normal)
|
|
2501
|
+
applyMarketButtonStyle(price, data: style?.dictionary("price"), defaultSize: 16, defaultLineHeight: 24, defaultWeight: .medium, color: nativeListColor(theme, "primaryText", "#202020"), defaultAlignment: .trailing)
|
|
2502
|
+
if !item.data.dictionaries("priceSegments").isEmpty {
|
|
2503
|
+
price.setAttributedTitle(marketAttributedText(item.data.string("price"), segments: item.data.dictionaries("priceSegments"), style: style?.dictionary("price"), defaultSize: 16, defaultLineHeight: 24, defaultWeight: .medium, color: nativeListColor(theme, "primaryText", "#202020"), defaultAlignment: marketTextAlignment("end")), for: .normal)
|
|
2504
|
+
}
|
|
2505
|
+
let changeData = item.data.dictionary("change") ?? [:]
|
|
2506
|
+
let change = accessoryButtons[1]
|
|
2507
|
+
change.isUserInteractionEnabled = false
|
|
2508
|
+
change.isHidden = false
|
|
2509
|
+
change.setAttributedTitle(nil, for: .normal)
|
|
2510
|
+
change.setTitle(changeData.string("text"), for: .normal)
|
|
2511
|
+
let defaultChangeColor = nativeListColor(theme, "inverseText", "#FFFFFF")
|
|
2512
|
+
applyMarketButtonStyle(change, data: style?.dictionary("change"), defaultSize: 14, defaultLineHeight: 20, defaultWeight: .medium, color: UIColor(nativeListHex: changeData.string("textColor", default: "#FFFFFF"), fallback: defaultChangeColor), defaultAlignment: .center)
|
|
2513
|
+
if !changeData.dictionaries("textSegments").isEmpty {
|
|
2514
|
+
let changeTextColor = UIColor(nativeListHex: changeData.string("textColor", default: ""), fallback: defaultChangeColor)
|
|
2515
|
+
change.setAttributedTitle(marketAttributedText(changeData.string("text"), segments: changeData.dictionaries("textSegments"), style: style?.dictionary("change"), defaultSize: 14, defaultLineHeight: 20, defaultWeight: .medium, color: changeTextColor, defaultAlignment: .center), for: .normal)
|
|
2516
|
+
}
|
|
2517
|
+
let toneKey = changeData.string("tone") == "positive" ? "positive" : changeData.string("tone") == "negative" ? "negative" : "secondaryText"
|
|
2518
|
+
change.backgroundColor = UIColor(nativeListHex: changeData.string("backgroundColor", default: ""), fallback: nativeListColor(theme, toneKey, changeData.string("tone") == "positive" ? "#218358" : changeData.string("tone") == "negative" ? "#CE2C31" : "#8D8D8D"))
|
|
2519
|
+
change.layer.cornerRadius = CGFloat(style?.double("changeCornerRadius", default: 8) ?? 8)
|
|
2520
|
+
change.clipsToBounds = true
|
|
2521
|
+
let width = change.widthAnchor.constraint(equalToConstant: CGFloat(style?.double("changeWidth", default: 80) ?? 80))
|
|
2522
|
+
let height = change.heightAnchor.constraint(equalToConstant: CGFloat(style?.double("changeHeight", default: 32) ?? 32))
|
|
2523
|
+
width.isActive = true
|
|
2524
|
+
height.isActive = true
|
|
2525
|
+
accessorySizeConstraints.append(contentsOf: [width, height])
|
|
2526
|
+
accessibilityLabel = item.data.string("accessibilityLabel", default: [item.data.string("title"), item.data.string("subtitle"), item.data.string("price"), changeData.string("text")].filter { !$0.isEmpty }.joined(separator: ", "))
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2017
2529
|
private func bindMediaTile(_ item: NativeListItem, theme: [String: Any]?) {
|
|
2018
2530
|
rootStack.axis = .vertical
|
|
2019
2531
|
rootStack.alignment = .fill
|
|
@@ -2630,6 +3142,114 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
2630
3142
|
rootStack.alignment = .center
|
|
2631
3143
|
rootStack.distribution = .fill
|
|
2632
3144
|
let variant = item.data.string("variant")
|
|
3145
|
+
let isMarket = item.data.string("presentation") == "market"
|
|
3146
|
+
if isMarket {
|
|
3147
|
+
rootLeadingConstraint.constant = 20
|
|
3148
|
+
rootTrailingConstraint.constant = -20
|
|
3149
|
+
rootTopConstraint.constant = 12
|
|
3150
|
+
rootBottomConstraint.constant = -12
|
|
3151
|
+
}
|
|
3152
|
+
if isMarket && variant == "retry" {
|
|
3153
|
+
let message = item.data.string("message")
|
|
3154
|
+
let text = item.data.string("actionText", default: "Retry")
|
|
3155
|
+
rootStack.axis = .vertical
|
|
3156
|
+
// The source tertiary Button has -5 vertical margins around its 30pt frame.
|
|
3157
|
+
rootStack.spacing = 7
|
|
3158
|
+
rootLeadingConstraint.constant = 32
|
|
3159
|
+
rootTrailingConstraint.constant = -32
|
|
3160
|
+
let height = CGFloat(item.data.double("height", default: message.isEmpty ? 52 : 120))
|
|
3161
|
+
let top = message.isEmpty ? 11 : max(32, (height - 56) / 2)
|
|
3162
|
+
rootTopConstraint.constant = top
|
|
3163
|
+
rootBottomConstraint.constant = -(height - top - (message.isEmpty ? 30 : 61))
|
|
3164
|
+
if !message.isEmpty {
|
|
3165
|
+
show(titleLabel, message, lines: 2)
|
|
3166
|
+
titleLabel.font = nativeListTabularFont(ofSize: 16)
|
|
3167
|
+
titleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
|
|
3168
|
+
titleLabel.textAlignment = .center
|
|
3169
|
+
setLineHeight(titleLabel, text: message, lineHeight: 24)
|
|
3170
|
+
rootStack.addArrangedSubview(mainStack)
|
|
3171
|
+
}
|
|
3172
|
+
showAccessory(0, text, action: (item.data.string("actionKey"), nil))
|
|
3173
|
+
let button = accessoryButtons[0]
|
|
3174
|
+
button.backgroundColor = .clear
|
|
3175
|
+
button.layer.cornerRadius = 15
|
|
3176
|
+
setButtonLine(button, text: text, font: nativeListTabularFont(ofSize: 14, weight: .medium),
|
|
3177
|
+
color: nativeListColor(theme, "secondaryText", "#646464"), lineHeight: 20)
|
|
3178
|
+
let textWidth = button.intrinsicContentSize.width
|
|
3179
|
+
selectorConstraints.append(contentsOf: [
|
|
3180
|
+
button.widthAnchor.constraint(equalToConstant: textWidth + 18),
|
|
3181
|
+
button.heightAnchor.constraint(equalToConstant: 30),
|
|
3182
|
+
])
|
|
3183
|
+
NSLayoutConstraint.activate(selectorConstraints)
|
|
3184
|
+
rootStack.addArrangedSubview(trailingStack)
|
|
3185
|
+
return
|
|
3186
|
+
}
|
|
3187
|
+
if variant == "loading" && item.data.string("loadingStyle") == "skeleton" {
|
|
3188
|
+
rootLeadingConstraint.constant = 20
|
|
3189
|
+
rootTrailingConstraint.constant = -20
|
|
3190
|
+
rootTopConstraint.constant = 12
|
|
3191
|
+
rootBottomConstraint.constant = -12
|
|
3192
|
+
let skeleton = NativeListMarketSkeleton(background: nativeListColor(theme, "background", "#FFFFFF"))
|
|
3193
|
+
skeleton.translatesAutoresizingMaskIntoConstraints = false
|
|
3194
|
+
skeleton.heightAnchor.constraint(equalToConstant: 32).isActive = true
|
|
3195
|
+
rootStack.addArrangedSubview(skeleton)
|
|
3196
|
+
selectorViews.append(skeleton)
|
|
3197
|
+
return
|
|
3198
|
+
}
|
|
3199
|
+
if variant == "loading" && item.data.string("loadingStyle") == "spinner" {
|
|
3200
|
+
rootTopConstraint.constant = 16
|
|
3201
|
+
rootBottomConstraint.constant = -16
|
|
3202
|
+
rootStack.addArrangedSubview(mainStack)
|
|
3203
|
+
mainStack.alignment = .center
|
|
3204
|
+
mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
3205
|
+
let indicator = UIActivityIndicatorView(style: .medium)
|
|
3206
|
+
indicator.color = nativeListColor(theme, "icon", "#0000009B")
|
|
3207
|
+
indicator.startAnimating()
|
|
3208
|
+
mainStack.addArrangedSubview(indicator)
|
|
3209
|
+
selectorViews.append(indicator)
|
|
3210
|
+
return
|
|
3211
|
+
}
|
|
3212
|
+
if isMarket && variant == "noMatch" {
|
|
3213
|
+
let padding = max(32, (CGFloat(item.data.double("height", default: 88)) - 24) / 2)
|
|
3214
|
+
rootTopConstraint.constant = padding
|
|
3215
|
+
rootBottomConstraint.constant = -padding
|
|
3216
|
+
rootStack.addArrangedSubview(mainStack)
|
|
3217
|
+
mainStack.alignment = .center
|
|
3218
|
+
mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
3219
|
+
titleLabel.font = nativeListTabularFont(ofSize: 16)
|
|
3220
|
+
titleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
|
|
3221
|
+
titleLabel.textAlignment = .center
|
|
3222
|
+
show(titleLabel, item.data.string("message"), lines: 1)
|
|
3223
|
+
setLineHeight(titleLabel, text: item.data.string("message"), lineHeight: 24)
|
|
3224
|
+
return
|
|
3225
|
+
}
|
|
3226
|
+
if isMarket && variant == "end" {
|
|
3227
|
+
rootTopConstraint.constant = 16
|
|
3228
|
+
rootBottomConstraint.constant = -16
|
|
3229
|
+
// OneKey patch: an empty title stack must not consume the dot's line height.
|
|
3230
|
+
titleRowStack.isHidden = true
|
|
3231
|
+
rootStack.addArrangedSubview(mainStack)
|
|
3232
|
+
mainStack.alignment = .center
|
|
3233
|
+
mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
3234
|
+
let indicator = UIStackView()
|
|
3235
|
+
indicator.axis = .horizontal
|
|
3236
|
+
indicator.alignment = .center
|
|
3237
|
+
indicator.spacing = 8
|
|
3238
|
+
for width in [CGFloat(80), 4, 80] {
|
|
3239
|
+
let mark = UIView()
|
|
3240
|
+
mark.translatesAutoresizingMaskIntoConstraints = false
|
|
3241
|
+
mark.backgroundColor = nativeListColor(theme, "separator", "#0000001F")
|
|
3242
|
+
mark.layer.cornerRadius = width == 4 ? 2 : 0
|
|
3243
|
+
NSLayoutConstraint.activate([
|
|
3244
|
+
mark.widthAnchor.constraint(equalToConstant: width),
|
|
3245
|
+
mark.heightAnchor.constraint(equalToConstant: width == 4 ? 4 : 1),
|
|
3246
|
+
])
|
|
3247
|
+
indicator.addArrangedSubview(mark)
|
|
3248
|
+
}
|
|
3249
|
+
mainStack.addArrangedSubview(indicator)
|
|
3250
|
+
selectorViews.append(indicator)
|
|
3251
|
+
return
|
|
3252
|
+
}
|
|
2633
3253
|
// OneKey patch: deprecated-wallet warnings stay inside the scrolling list.
|
|
2634
3254
|
if variant == "warning" {
|
|
2635
3255
|
rootStack.addArrangedSubview(mainStack)
|
|
@@ -2661,10 +3281,10 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
2661
3281
|
return
|
|
2662
3282
|
}
|
|
2663
3283
|
if variant == "loading" {
|
|
2664
|
-
leadingWidth.constant = 40
|
|
2665
|
-
leadingHeight.constant = 40
|
|
3284
|
+
leadingWidth.constant = isMarket ? 32 : 40
|
|
3285
|
+
leadingHeight.constant = isMarket ? 32 : 40
|
|
2666
3286
|
leadingContainer.backgroundColor = nativeListColor(theme, "strongBackground", "#F0F0F0")
|
|
2667
|
-
leadingContainer.layer.cornerRadius = 20
|
|
3287
|
+
leadingContainer.layer.cornerRadius = isMarket ? 16 : 20
|
|
2668
3288
|
rootStack.addArrangedSubview(leadingContainer)
|
|
2669
3289
|
configureSkeleton(skeletonPrimary, width: 120, height: 12, theme: theme)
|
|
2670
3290
|
configureSkeleton(skeletonSecondary, width: 80, height: 12, theme: theme)
|
|
@@ -3076,8 +3696,8 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3076
3696
|
let paragraphStyle = NSMutableParagraphStyle()
|
|
3077
3697
|
paragraphStyle.minimumLineHeight = lineHeight
|
|
3078
3698
|
paragraphStyle.maximumLineHeight = lineHeight
|
|
3079
|
-
if currentItem?.data.string("presentation") == "walletSidebar" {
|
|
3080
|
-
//
|
|
3699
|
+
if currentItem?.type == "market" || currentItem?.data.string("presentation") == "walletSidebar" {
|
|
3700
|
+
// Attributed paragraphs must preserve the label alignment and tail ellipsis.
|
|
3081
3701
|
paragraphStyle.alignment = label.textAlignment
|
|
3082
3702
|
paragraphStyle.lineBreakMode = label.lineBreakMode
|
|
3083
3703
|
}
|
|
@@ -3086,15 +3706,15 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3086
3706
|
.foregroundColor: label.textColor as Any,
|
|
3087
3707
|
.paragraphStyle: paragraphStyle,
|
|
3088
3708
|
]
|
|
3089
|
-
if (currentItem?.data["height"] != nil && (["accountSelector", "walletSidebar"].contains(currentItem?.data.string("presentation") ?? "") || currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector")) || currentItem?.type == "system" && currentItem?.data.string("variant") == "warning" {
|
|
3709
|
+
if currentItem?.type == "market" || currentItem?.type == "system" && currentItem?.data.string("presentation") == "market" && currentItem?.data.string("variant") == "noMatch" || (currentItem?.data["height"] != nil && (["accountSelector", "walletSidebar"].contains(currentItem?.data.string("presentation") ?? "") || currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector")) || currentItem?.type == "system" && currentItem?.data.string("variant") == "warning" {
|
|
3090
3710
|
// OneKey patch: React Native centers font metrics inside explicit line heights.
|
|
3091
3711
|
let baselineOffset = max(0, (lineHeight - label.font.lineHeight) / 2)
|
|
3092
3712
|
// OneKey patch: TextKit's 14/20 headings align their baseline to the upper physical pixel.
|
|
3093
|
-
let isSelectorHeading = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" &&
|
|
3713
|
+
let isSelectorHeading = lineHeight == 20 && (currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" || currentItem?.type == "market" && label.font.pointSize == 14)
|
|
3094
3714
|
let scale = window?.screen.scale ?? traitCollection.displayScale
|
|
3095
3715
|
attributes[.baselineOffset] = isSelectorHeading && scale > 0 ? ceil(baselineOffset * scale) / scale : baselineOffset
|
|
3096
3716
|
}
|
|
3097
|
-
if letterSpacing != 0 { attributes[.kern] = letterSpacing }
|
|
3717
|
+
if letterSpacing != 0 || currentItem?.type == "market" || currentItem?.data.string("presentation") == "market" { attributes[.kern] = letterSpacing }
|
|
3098
3718
|
label.attributedText = NSAttributedString(string: text, attributes: attributes)
|
|
3099
3719
|
}
|
|
3100
3720
|
|
|
@@ -3109,11 +3729,14 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3109
3729
|
let paragraphStyle = NSMutableParagraphStyle()
|
|
3110
3730
|
paragraphStyle.minimumLineHeight = lineHeight
|
|
3111
3731
|
paragraphStyle.maximumLineHeight = lineHeight
|
|
3732
|
+
let isMarketText = currentItem?.type == "market" || currentItem?.type == "system" && currentItem?.data.string("presentation") == "market" && currentItem?.data.string("variant") == "retry"
|
|
3112
3733
|
let isSelectorValue = currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") != "summary"
|
|
3113
3734
|
let isSelectorSummary = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") == "summary"
|
|
3114
3735
|
(button as? NativeListAccessoryButton)?.selectorSummaryLineHeight = isSelectorSummary ? lineHeight : nil
|
|
3736
|
+
(button as? NativeListAccessoryButton)?.marketLineHeight = isMarketText ? lineHeight : nil
|
|
3115
3737
|
// OneKey patch: summary text uses its source line box; currency retains trailing alignment.
|
|
3116
|
-
|
|
3738
|
+
// Market's line box already handles alignment; source text starts at its origin.
|
|
3739
|
+
paragraphStyle.alignment = isSelectorValue ? .right : isSelectorSummary || isMarketText ? .natural : .center
|
|
3117
3740
|
if isSelectorSummary {
|
|
3118
3741
|
button.contentHorizontalAlignment = .leading
|
|
3119
3742
|
button.titleLabel?.textAlignment = .natural
|
|
@@ -3122,17 +3745,16 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3122
3745
|
button.contentHorizontalAlignment = .trailing
|
|
3123
3746
|
button.titleLabel?.textAlignment = .right
|
|
3124
3747
|
}
|
|
3125
|
-
let baselineOffset: CGFloat = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil ? max(0, (lineHeight - font.lineHeight) / 2) : 0
|
|
3748
|
+
let baselineOffset: CGFloat = isMarketText || currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil ? max(0, (lineHeight - font.lineHeight) / 2) : 0
|
|
3749
|
+
var attributes: [NSAttributedString.Key: Any] = [
|
|
3750
|
+
.font: font,
|
|
3751
|
+
.foregroundColor: color,
|
|
3752
|
+
.paragraphStyle: paragraphStyle,
|
|
3753
|
+
.baselineOffset: isMarketText && lineHeight == 20 && font.pointSize == 14 ? ceil(baselineOffset * max(1, traitCollection.displayScale)) / max(1, traitCollection.displayScale) : baselineOffset,
|
|
3754
|
+
]
|
|
3755
|
+
if isMarketText { attributes[.kern] = 0 }
|
|
3126
3756
|
button.setAttributedTitle(
|
|
3127
|
-
NSAttributedString(
|
|
3128
|
-
string: text,
|
|
3129
|
-
attributes: [
|
|
3130
|
-
.font: font,
|
|
3131
|
-
.foregroundColor: color,
|
|
3132
|
-
.paragraphStyle: paragraphStyle,
|
|
3133
|
-
.baselineOffset: baselineOffset,
|
|
3134
|
-
]
|
|
3135
|
-
),
|
|
3757
|
+
NSAttributedString(string: text, attributes: attributes),
|
|
3136
3758
|
for: .normal
|
|
3137
3759
|
)
|
|
3138
3760
|
}
|
|
@@ -3394,6 +4016,19 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3394
4016
|
)
|
|
3395
4017
|
}
|
|
3396
4018
|
|
|
4019
|
+
@objc private func marketBadgePressed(_ sender: UIButton) {
|
|
4020
|
+
guard let item = currentItem,
|
|
4021
|
+
marketBadgeActionKeys.indices.contains(sender.tag),
|
|
4022
|
+
let actionKey = marketBadgeActionKeys[sender.tag],
|
|
4023
|
+
!actionKey.isEmpty else { return }
|
|
4024
|
+
onAction?(
|
|
4025
|
+
item,
|
|
4026
|
+
actionKey,
|
|
4027
|
+
nil,
|
|
4028
|
+
actionOrigin(sourceView: sender, source: "marketBadge", slot: sender.tag)
|
|
4029
|
+
)
|
|
4030
|
+
}
|
|
4031
|
+
|
|
3397
4032
|
@objc private func footerActionPressed(_ sender: UIButton) {
|
|
3398
4033
|
guard let item = currentItem, footerActionKeys.indices.contains(sender.tag) else { return }
|
|
3399
4034
|
onAction?(
|