expo-tvos-search 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +111 -0
- package/README.md +47 -282
- package/build/index.d.ts +57 -5
- package/build/index.d.ts.map +1 -1
- package/ios/CachedAsyncImage.swift +78 -0
- package/ios/ExpoTvosSearchModule.swift +31 -7
- package/ios/ExpoTvosSearchView.swift +250 -421
- package/ios/HexColorParser.swift +55 -0
- package/ios/MarqueeText.swift +28 -51
- package/ios/SearchResultCard.swift +172 -0
- package/ios/SearchResultItem.swift +8 -0
- package/ios/Tests/HexColorParserTests.swift +183 -0
- package/ios/Tests/SearchViewModelTests.swift +1 -1
- package/ios/TvosSearchContentView.swift +125 -0
- package/package.json +2 -1
- package/src/__tests__/events.test.ts +128 -0
- package/src/__tests__/index.test.tsx +47 -1
- package/src/index.tsx +61 -5
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":";AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE;QACX,0DAA0D;QAC1D,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE;QACX,0DAA0D;QAC1D,EAAE,EAAE,MAAM,CAAC;KACZ,CAAC;CACH;AAED;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAC/B,oBAAoB,GACpB,mBAAmB,GACnB,mBAAmB,GACnB,SAAS,CAAC;AAEd;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE;QACX,sDAAsD;QACtD,QAAQ,EAAE,uBAAuB,CAAC;QAClC,mCAAmC;QACnC,OAAO,EAAE,MAAM,CAAC;QAChB,yDAAyD;QACzD,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE;QACX,iCAAiC;QACjC,IAAI,EAAE,iBAAiB,GAAG,eAAe,GAAG,aAAa,GAAG,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#if os(tvOS)
|
|
2
|
+
|
|
3
|
+
import SwiftUI
|
|
4
|
+
|
|
5
|
+
/// Thread-safe NSCache-backed image cache singleton.
|
|
6
|
+
/// Auto-evicts under memory pressure via NSCache's built-in LRU behavior.
|
|
7
|
+
final class ImageCache {
|
|
8
|
+
static let shared = ImageCache()
|
|
9
|
+
|
|
10
|
+
private let cache = NSCache<NSURL, UIImage>()
|
|
11
|
+
|
|
12
|
+
private init() {
|
|
13
|
+
cache.countLimit = 100
|
|
14
|
+
cache.totalCostLimit = 100 * 1024 * 1024 // 100 MB
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func image(for url: URL) -> UIImage? {
|
|
18
|
+
cache.object(forKey: url as NSURL)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func setImage(_ image: UIImage, for url: URL) {
|
|
22
|
+
let cost = image.cgImage.map { $0.bytesPerRow * $0.height } ?? 0
|
|
23
|
+
cache.setObject(image, forKey: url as NSURL, cost: cost)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// SwiftUI view that loads images with NSCache backing.
|
|
28
|
+
/// Cached images appear instantly (no loading flash).
|
|
29
|
+
/// On failure, renders EmptyView so the parent placeholder shows through.
|
|
30
|
+
struct CachedAsyncImage: View {
|
|
31
|
+
let url: URL
|
|
32
|
+
let contentMode: ContentMode
|
|
33
|
+
let width: CGFloat
|
|
34
|
+
let height: CGFloat
|
|
35
|
+
|
|
36
|
+
@State private var uiImage: UIImage?
|
|
37
|
+
@State private var isLoading = true
|
|
38
|
+
|
|
39
|
+
var body: some View {
|
|
40
|
+
Group {
|
|
41
|
+
if let uiImage = uiImage {
|
|
42
|
+
Image(uiImage: uiImage)
|
|
43
|
+
.resizable()
|
|
44
|
+
.aspectRatio(contentMode: contentMode)
|
|
45
|
+
.frame(width: width, height: height)
|
|
46
|
+
} else if isLoading {
|
|
47
|
+
ProgressView()
|
|
48
|
+
} else {
|
|
49
|
+
// Failure — show nothing, parent placeholder shows through
|
|
50
|
+
EmptyView()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
.frame(width: width, height: height)
|
|
54
|
+
.task(id: url) {
|
|
55
|
+
uiImage = nil
|
|
56
|
+
isLoading = true
|
|
57
|
+
|
|
58
|
+
if let cached = ImageCache.shared.image(for: url) {
|
|
59
|
+
uiImage = cached
|
|
60
|
+
isLoading = false
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
do {
|
|
65
|
+
let (data, _) = try await URLSession.shared.data(from: url)
|
|
66
|
+
if let image = UIImage(data: data) {
|
|
67
|
+
ImageCache.shared.setImage(image, for: url)
|
|
68
|
+
uiImage = image
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// Cancelled or network error — placeholder shows through
|
|
72
|
+
}
|
|
73
|
+
isLoading = false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#endif
|
|
@@ -8,11 +8,28 @@ public class ExpoTvosSearchModule: Module {
|
|
|
8
8
|
private static let maxMarqueeDelay: Double = 60.0
|
|
9
9
|
private static let maxStringLength = 500
|
|
10
10
|
|
|
11
|
+
/// Truncates a string to maxStringLength and emits a validation warning if truncation occurred.
|
|
12
|
+
private static func truncateString(
|
|
13
|
+
_ value: String,
|
|
14
|
+
propName: String,
|
|
15
|
+
view: ExpoTvosSearchView
|
|
16
|
+
) -> String {
|
|
17
|
+
let truncated = String(value.prefix(maxStringLength))
|
|
18
|
+
if truncated.count < value.count {
|
|
19
|
+
view.onValidationWarning([
|
|
20
|
+
"type": "value_truncated",
|
|
21
|
+
"message": "\(propName) truncated to \(maxStringLength) characters",
|
|
22
|
+
"context": "original length: \(value.count)"
|
|
23
|
+
])
|
|
24
|
+
}
|
|
25
|
+
return truncated
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
public func definition() -> ModuleDefinition {
|
|
12
29
|
Name("ExpoTvosSearch")
|
|
13
30
|
|
|
14
31
|
View(ExpoTvosSearchView.self) {
|
|
15
|
-
Events("onSearch", "onSelectItem", "onError", "onValidationWarning")
|
|
32
|
+
Events("onSearch", "onSelectItem", "onError", "onValidationWarning", "onSearchFieldFocused", "onSearchFieldBlurred")
|
|
16
33
|
|
|
17
34
|
Prop("results") { (view: ExpoTvosSearchView, results: [[String: Any]]) in
|
|
18
35
|
// Limit results array size to prevent memory issues
|
|
@@ -41,8 +58,15 @@ public class ExpoTvosSearchModule: Module {
|
|
|
41
58
|
}
|
|
42
59
|
|
|
43
60
|
Prop("placeholder") { (view: ExpoTvosSearchView, placeholder: String) in
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
view.placeholder = Self.truncateString(placeholder, propName: "placeholder", view: view)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Prop("searchText") { (view: ExpoTvosSearchView, text: String?) in
|
|
65
|
+
if let text = text {
|
|
66
|
+
view.searchTextProp = Self.truncateString(text, propName: "searchText", view: view)
|
|
67
|
+
} else {
|
|
68
|
+
view.searchTextProp = nil
|
|
69
|
+
}
|
|
46
70
|
}
|
|
47
71
|
|
|
48
72
|
Prop("isLoading") { (view: ExpoTvosSearchView, isLoading: Bool) in
|
|
@@ -96,19 +120,19 @@ public class ExpoTvosSearchModule: Module {
|
|
|
96
120
|
}
|
|
97
121
|
|
|
98
122
|
Prop("emptyStateText") { (view: ExpoTvosSearchView, text: String) in
|
|
99
|
-
view.emptyStateText =
|
|
123
|
+
view.emptyStateText = Self.truncateString(text, propName: "emptyStateText", view: view)
|
|
100
124
|
}
|
|
101
125
|
|
|
102
126
|
Prop("searchingText") { (view: ExpoTvosSearchView, text: String) in
|
|
103
|
-
view.searchingText =
|
|
127
|
+
view.searchingText = Self.truncateString(text, propName: "searchingText", view: view)
|
|
104
128
|
}
|
|
105
129
|
|
|
106
130
|
Prop("noResultsText") { (view: ExpoTvosSearchView, text: String) in
|
|
107
|
-
view.noResultsText =
|
|
131
|
+
view.noResultsText = Self.truncateString(text, propName: "noResultsText", view: view)
|
|
108
132
|
}
|
|
109
133
|
|
|
110
134
|
Prop("noResultsHintText") { (view: ExpoTvosSearchView, text: String) in
|
|
111
|
-
view.noResultsHintText =
|
|
135
|
+
view.noResultsHintText = Self.truncateString(text, propName: "noResultsHintText", view: view)
|
|
112
136
|
}
|
|
113
137
|
|
|
114
138
|
Prop("textColor") { (view: ExpoTvosSearchView, colorHex: String?) in
|