expo-tiddlywiki-filesystem-android-external-storage 2.2.10 → 2.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/expo/modules/externalstorage/ExternalStorageModule.kt +29 -0
- package/android/src/main/java/expo/modules/externalstorage/GitHelper.kt +1 -11
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/expo-module.config.json +4 -1
- package/ios/ExternalStorageModule.swift +359 -0
- package/ios/GitHelper.swift +697 -0
- package/ios/TarExtractor.swift +158 -0
- package/ios/TiddlerParser.swift +254 -0
- package/package.json +3 -2
- package/src/index.ts +2 -2
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Minimal tar archive extractor — iOS equivalent of the Kotlin implementation.
|
|
4
|
+
/// Supports POSIX ustar, GNU long-name (type 'L'), and POSIX pax (type 'x'/'g') extensions.
|
|
5
|
+
enum TarExtractor {
|
|
6
|
+
|
|
7
|
+
private static let blockSize = 512
|
|
8
|
+
|
|
9
|
+
/// Extract an uncompressed tar archive to a destination directory.
|
|
10
|
+
/// Returns the number of files extracted.
|
|
11
|
+
static func extract(tarPath: String, destDir: String) throws -> Int {
|
|
12
|
+
let fm = FileManager.default
|
|
13
|
+
try fm.createDirectory(atPath: destDir, withIntermediateDirectories: true, attributes: nil)
|
|
14
|
+
|
|
15
|
+
guard let handle = FileHandle(forReadingAtPath: tarPath) else {
|
|
16
|
+
throw NSError(domain: "ENOENT", code: 2, userInfo: [NSLocalizedDescriptionKey: "Tar file not found: \(tarPath)"])
|
|
17
|
+
}
|
|
18
|
+
defer { handle.closeFile() }
|
|
19
|
+
|
|
20
|
+
let destURL = URL(fileURLWithPath: destDir).standardized
|
|
21
|
+
var filesExtracted = 0
|
|
22
|
+
var longName: String?
|
|
23
|
+
|
|
24
|
+
while true {
|
|
25
|
+
let headerData = handle.readData(ofLength: blockSize)
|
|
26
|
+
if headerData.count < blockSize { break }
|
|
27
|
+
|
|
28
|
+
// Check for end-of-archive (two consecutive zero blocks)
|
|
29
|
+
if headerData.allSatisfy({ $0 == 0 }) { break }
|
|
30
|
+
|
|
31
|
+
// Extract filename from header
|
|
32
|
+
let rawName = extractString(from: headerData, offset: 0, maxLen: 100)
|
|
33
|
+
let typeFlag = headerData.count > 156 ? headerData[156] : 0
|
|
34
|
+
|
|
35
|
+
// Parse file size from octal
|
|
36
|
+
let sizeString = extractString(from: headerData, offset: 124, maxLen: 12)
|
|
37
|
+
let fileSize = UInt64(sizeString.trimmingCharacters(in: .whitespaces), radix: 8) ?? 0
|
|
38
|
+
|
|
39
|
+
// POSIX ustar prefix
|
|
40
|
+
let prefix = extractString(from: headerData, offset: 345, maxLen: 155)
|
|
41
|
+
|
|
42
|
+
// Handle special entry types
|
|
43
|
+
if typeFlag == UInt8(ascii: "L") {
|
|
44
|
+
// GNU long name
|
|
45
|
+
let nameData = readExactly(handle: handle, count: Int(fileSize))
|
|
46
|
+
longName = String(data: nameData, encoding: .utf8)?.trimmingCharacters(in: CharacterSet(charactersIn: "\0"))
|
|
47
|
+
skipToBlockBoundary(handle: handle, fileSize: fileSize)
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if typeFlag == UInt8(ascii: "x") || typeFlag == UInt8(ascii: "g") {
|
|
52
|
+
// POSIX pax extended header — parse for "path" keyword
|
|
53
|
+
let paxData = readExactly(handle: handle, count: Int(fileSize))
|
|
54
|
+
skipToBlockBoundary(handle: handle, fileSize: fileSize)
|
|
55
|
+
if let paxStr = String(data: paxData, encoding: .utf8) {
|
|
56
|
+
for line in paxStr.components(separatedBy: "\n") {
|
|
57
|
+
// Format: "len key=value\n"
|
|
58
|
+
if let eqIndex = line.firstIndex(of: "=") {
|
|
59
|
+
let beforeEq = line[line.startIndex..<eqIndex]
|
|
60
|
+
if let spaceIndex = beforeEq.lastIndex(of: " ") {
|
|
61
|
+
let key = String(beforeEq[beforeEq.index(after: spaceIndex)...])
|
|
62
|
+
if key == "path" {
|
|
63
|
+
longName = String(line[line.index(after: eqIndex)...])
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Skip directories and non-regular files
|
|
73
|
+
if typeFlag == UInt8(ascii: "5") || typeFlag == UInt8(ascii: "2") {
|
|
74
|
+
skipDataBlocks(handle: handle, fileSize: fileSize)
|
|
75
|
+
longName = nil
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Determine final filename
|
|
80
|
+
let entryName: String
|
|
81
|
+
if let ln = longName {
|
|
82
|
+
entryName = ln
|
|
83
|
+
longName = nil
|
|
84
|
+
} else if !prefix.isEmpty {
|
|
85
|
+
entryName = "\(prefix)/\(rawName)"
|
|
86
|
+
} else {
|
|
87
|
+
entryName = rawName
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
guard !entryName.isEmpty else {
|
|
91
|
+
skipDataBlocks(handle: handle, fileSize: fileSize)
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Path traversal protection
|
|
96
|
+
let destFile = destURL.appendingPathComponent(entryName).standardized
|
|
97
|
+
guard destFile.path.hasPrefix(destURL.path) else {
|
|
98
|
+
skipDataBlocks(handle: handle, fileSize: fileSize)
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Create parent directories
|
|
103
|
+
try fm.createDirectory(
|
|
104
|
+
at: destFile.deletingLastPathComponent(),
|
|
105
|
+
withIntermediateDirectories: true,
|
|
106
|
+
attributes: nil
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
// Read file content
|
|
110
|
+
let contentData = readExactly(handle: handle, count: Int(fileSize))
|
|
111
|
+
skipToBlockBoundary(handle: handle, fileSize: fileSize)
|
|
112
|
+
try contentData.write(to: destFile)
|
|
113
|
+
filesExtracted += 1
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return filesExtracted
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
private static func extractString(from data: Data, offset: Int, maxLen: Int) -> String {
|
|
122
|
+
let end = min(offset + maxLen, data.count)
|
|
123
|
+
guard offset < end else { return "" }
|
|
124
|
+
let slice = data[offset..<end]
|
|
125
|
+
// Find null terminator
|
|
126
|
+
let nullIndex = slice.firstIndex(of: 0) ?? end
|
|
127
|
+
let stringSlice = data[offset..<nullIndex]
|
|
128
|
+
return String(data: stringSlice, encoding: .utf8) ?? ""
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private static func readExactly(handle: FileHandle, count: Int) -> Data {
|
|
132
|
+
guard count > 0 else { return Data() }
|
|
133
|
+
var result = Data()
|
|
134
|
+
var remaining = count
|
|
135
|
+
while remaining > 0 {
|
|
136
|
+
let chunk = handle.readData(ofLength: remaining)
|
|
137
|
+
if chunk.isEmpty { break }
|
|
138
|
+
result.append(chunk)
|
|
139
|
+
remaining -= chunk.count
|
|
140
|
+
}
|
|
141
|
+
return result
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private static func skipDataBlocks(handle: FileHandle, fileSize: UInt64) {
|
|
145
|
+
guard fileSize > 0 else { return }
|
|
146
|
+
let totalBytes = ((fileSize + UInt64(blockSize - 1)) / UInt64(blockSize)) * UInt64(blockSize)
|
|
147
|
+
handle.seek(toFileOffset: handle.offsetInFile + totalBytes)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private static func skipToBlockBoundary(handle: FileHandle, fileSize: UInt64) {
|
|
151
|
+
guard fileSize > 0 else { return }
|
|
152
|
+
let remainder = fileSize % UInt64(blockSize)
|
|
153
|
+
if remainder > 0 {
|
|
154
|
+
let padding = UInt64(blockSize) - remainder
|
|
155
|
+
handle.seek(toFileOffset: handle.offsetInFile + padding)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// TiddlyWiki batch file parser — iOS equivalent of the Kotlin implementation.
|
|
4
|
+
/// Parses .tid, .json, and .meta files and returns a JSON array string.
|
|
5
|
+
enum TiddlerParser {
|
|
6
|
+
|
|
7
|
+
/// Parse a batch of tiddler files and return a JSON array string.
|
|
8
|
+
static func batchParse(filePaths: [String], quickLoadMode: Bool) -> String {
|
|
9
|
+
let fm = FileManager.default
|
|
10
|
+
var results = [[String: Any]]()
|
|
11
|
+
|
|
12
|
+
for path in filePaths {
|
|
13
|
+
guard fm.fileExists(atPath: path) else { continue }
|
|
14
|
+
let filename = (path as NSString).lastPathComponent
|
|
15
|
+
let ext = (filename as NSString).pathExtension.lowercased()
|
|
16
|
+
|
|
17
|
+
switch ext {
|
|
18
|
+
case "tid":
|
|
19
|
+
if let tiddler = parseDotTid(path: path, quickLoadMode: quickLoadMode) {
|
|
20
|
+
results.append(tiddler)
|
|
21
|
+
}
|
|
22
|
+
case "json":
|
|
23
|
+
if let parsed = parseDotJson(path: path) {
|
|
24
|
+
if let array = parsed as? [[String: Any]] {
|
|
25
|
+
results.append(contentsOf: array)
|
|
26
|
+
} else if let single = parsed as? [String: Any] {
|
|
27
|
+
results.append(single)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
case "meta":
|
|
31
|
+
if let tiddler = parseDotMeta(path: path, quickLoadMode: quickLoadMode) {
|
|
32
|
+
results.append(tiddler)
|
|
33
|
+
}
|
|
34
|
+
default:
|
|
35
|
+
break
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
guard let jsonData = try? JSONSerialization.data(withJSONObject: results, options: []) else {
|
|
40
|
+
return "[]"
|
|
41
|
+
}
|
|
42
|
+
return String(data: jsonData, encoding: .utf8) ?? "[]"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── .tid parser ─────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
private static func parseDotTid(path: String, quickLoadMode: Bool) -> [String: Any]? {
|
|
48
|
+
guard let content = try? String(contentsOfFile: path, encoding: .utf8) else { return nil }
|
|
49
|
+
|
|
50
|
+
var fields = [String: Any]()
|
|
51
|
+
|
|
52
|
+
// Find the first blank line separating headers from body
|
|
53
|
+
let blankLinePattern = try? NSRegularExpression(pattern: "\\r?\\n\\r?\\n")
|
|
54
|
+
let range = NSRange(content.startIndex..<content.endIndex, in: content)
|
|
55
|
+
let match = blankLinePattern?.firstMatch(in: content, range: range)
|
|
56
|
+
|
|
57
|
+
let headerText: String
|
|
58
|
+
let bodyOffset: Int
|
|
59
|
+
let estimatedBodyLength: Int
|
|
60
|
+
|
|
61
|
+
if let match = match {
|
|
62
|
+
let headerRange = content.startIndex..<content.index(content.startIndex, offsetBy: match.range.location)
|
|
63
|
+
headerText = String(content[headerRange])
|
|
64
|
+
bodyOffset = match.range.location + match.range.length
|
|
65
|
+
estimatedBodyLength = content.utf8.count - bodyOffset
|
|
66
|
+
} else {
|
|
67
|
+
headerText = content
|
|
68
|
+
bodyOffset = -1
|
|
69
|
+
estimatedBodyLength = 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Parse header lines
|
|
73
|
+
for line in headerText.components(separatedBy: .newlines) {
|
|
74
|
+
guard let colonIndex = line.firstIndex(of: ":") else { continue }
|
|
75
|
+
let fieldName = String(line[line.startIndex..<colonIndex]).trimmingCharacters(in: .whitespaces)
|
|
76
|
+
let fieldValue = String(line[line.index(after: colonIndex)...]).trimmingCharacters(in: .whitespaces)
|
|
77
|
+
if !fieldName.isEmpty {
|
|
78
|
+
fields[fieldName] = fieldValue
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Use filename as title fallback
|
|
83
|
+
if fields["title"] == nil {
|
|
84
|
+
fields["title"] = getTitleFromFilename((path as NSString).lastPathComponent)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let title = fields["title"] as? String ?? ""
|
|
88
|
+
let type = fields["type"] as? String ?? ""
|
|
89
|
+
let hasModuleType = fields["module-type"] != nil
|
|
90
|
+
let hasPluginType = fields["plugin-type"] != nil
|
|
91
|
+
|
|
92
|
+
// Determine if we should include full text
|
|
93
|
+
let shouldIncludeText: Bool
|
|
94
|
+
if quickLoadMode {
|
|
95
|
+
shouldIncludeText = shouldPreserveFullTextInQuickLoad(
|
|
96
|
+
title: title, type: type, hasModuleType: hasModuleType, hasPluginType: hasPluginType
|
|
97
|
+
)
|
|
98
|
+
} else {
|
|
99
|
+
shouldIncludeText = shouldSaveFullTiddler(
|
|
100
|
+
title: title, type: type, hasModuleType: hasModuleType, hasPluginType: hasPluginType,
|
|
101
|
+
estimatedTextLength: estimatedBodyLength
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if shouldIncludeText, bodyOffset >= 0, estimatedBodyLength > 0 {
|
|
106
|
+
let bodyStartIndex = content.index(content.startIndex, offsetBy: bodyOffset)
|
|
107
|
+
fields["text"] = String(content[bodyStartIndex...])
|
|
108
|
+
} else if !shouldIncludeText {
|
|
109
|
+
fields.removeValue(forKey: "text")
|
|
110
|
+
fields["_is_skinny"] = "yes"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return fields
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── .json parser ────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
private static func parseDotJson(path: String) -> Any? {
|
|
119
|
+
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return nil }
|
|
120
|
+
|
|
121
|
+
guard let json = try? JSONSerialization.jsonObject(with: data) else { return nil }
|
|
122
|
+
|
|
123
|
+
if let array = json as? [[String: Any]] {
|
|
124
|
+
let filtered = array.filter { $0["title"] != nil }
|
|
125
|
+
return filtered.isEmpty ? nil : filtered
|
|
126
|
+
} else if let obj = json as? [String: Any] {
|
|
127
|
+
if obj["title"] != nil {
|
|
128
|
+
return obj
|
|
129
|
+
}
|
|
130
|
+
// Plugin bundle format — skip
|
|
131
|
+
return nil
|
|
132
|
+
}
|
|
133
|
+
return nil
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── .meta parser ───────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
private static let textCompanionExtensions: Set<String> = [
|
|
139
|
+
"json", "js", "css", "svg", "txt", "html", "htm"
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
private static func parseDotMeta(path: String, quickLoadMode: Bool) -> [String: Any]? {
|
|
143
|
+
guard let metaContent = try? String(contentsOfFile: path, encoding: .utf8) else { return nil }
|
|
144
|
+
|
|
145
|
+
var fields = [String: Any]()
|
|
146
|
+
|
|
147
|
+
for line in metaContent.components(separatedBy: .newlines) {
|
|
148
|
+
guard let colonIndex = line.firstIndex(of: ":") else { continue }
|
|
149
|
+
let fieldName = String(line[line.startIndex..<colonIndex]).trimmingCharacters(in: .whitespaces)
|
|
150
|
+
let fieldValue = String(line[line.index(after: colonIndex)...]).trimmingCharacters(in: .whitespaces)
|
|
151
|
+
if !fieldName.isEmpty {
|
|
152
|
+
fields[fieldName] = fieldValue
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if fields["title"] == nil {
|
|
157
|
+
let metaFilename = (path as NSString).lastPathComponent
|
|
158
|
+
let baseName = (metaFilename as NSString).deletingPathExtension // removes .meta
|
|
159
|
+
fields["title"] = getTitleFromFilename(baseName)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Find companion file
|
|
163
|
+
let companionPath = (path as NSString).deletingPathExtension // removes .meta
|
|
164
|
+
let fm = FileManager.default
|
|
165
|
+
|
|
166
|
+
if fm.fileExists(atPath: companionPath) {
|
|
167
|
+
let companionExt = (companionPath as NSString).pathExtension.lowercased()
|
|
168
|
+
|
|
169
|
+
if companionExt == "json" {
|
|
170
|
+
// .meta + .json pair
|
|
171
|
+
let title = fields["title"] as? String ?? ""
|
|
172
|
+
let type = fields["type"] as? String ?? ""
|
|
173
|
+
let hasModuleType = fields["module-type"] != nil
|
|
174
|
+
let hasPluginType = fields["plugin-type"] != nil
|
|
175
|
+
|
|
176
|
+
let shouldIncludeText: Bool
|
|
177
|
+
if quickLoadMode {
|
|
178
|
+
shouldIncludeText = shouldPreserveFullTextInQuickLoad(
|
|
179
|
+
title: title, type: type, hasModuleType: hasModuleType, hasPluginType: hasPluginType
|
|
180
|
+
)
|
|
181
|
+
} else {
|
|
182
|
+
shouldIncludeText = true
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if shouldIncludeText {
|
|
186
|
+
if let jsonContent = try? String(contentsOfFile: companionPath, encoding: .utf8) {
|
|
187
|
+
fields["text"] = jsonContent
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
fields["_is_skinny"] = "yes"
|
|
191
|
+
}
|
|
192
|
+
} else if textCompanionExtensions.contains(companionExt) {
|
|
193
|
+
// Text companion — include if not quick-load-skinny
|
|
194
|
+
let title = fields["title"] as? String ?? ""
|
|
195
|
+
let type = fields["type"] as? String ?? ""
|
|
196
|
+
let hasModuleType = fields["module-type"] != nil
|
|
197
|
+
let hasPluginType = fields["plugin-type"] != nil
|
|
198
|
+
|
|
199
|
+
let shouldIncludeText: Bool
|
|
200
|
+
if quickLoadMode {
|
|
201
|
+
shouldIncludeText = shouldPreserveFullTextInQuickLoad(
|
|
202
|
+
title: title, type: type, hasModuleType: hasModuleType, hasPluginType: hasPluginType
|
|
203
|
+
)
|
|
204
|
+
} else {
|
|
205
|
+
shouldIncludeText = true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if shouldIncludeText {
|
|
209
|
+
if let textContent = try? String(contentsOfFile: companionPath, encoding: .utf8) {
|
|
210
|
+
fields["text"] = textContent
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
fields["_is_skinny"] = "yes"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Binary companions (images etc.) — no text set
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return fields["title"] != nil ? fields : nil
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
static func getTitleFromFilename(_ filename: String) -> String {
|
|
225
|
+
var name = filename
|
|
226
|
+
for ext in [".tid", ".json", ".meta"] {
|
|
227
|
+
if name.lowercased().hasSuffix(ext) {
|
|
228
|
+
name = String(name.dropLast(ext.count))
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return name
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
static func shouldSaveFullTiddler(
|
|
236
|
+
title: String, type: String, hasModuleType: Bool, hasPluginType: Bool,
|
|
237
|
+
estimatedTextLength: Int
|
|
238
|
+
) -> Bool {
|
|
239
|
+
if shouldPreserveFullTextInQuickLoad(title: title, type: type, hasModuleType: hasModuleType, hasPluginType: hasPluginType) {
|
|
240
|
+
return true
|
|
241
|
+
}
|
|
242
|
+
if estimatedTextLength < 10000 { return true }
|
|
243
|
+
return false
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
static func shouldPreserveFullTextInQuickLoad(
|
|
247
|
+
title: String, type: String, hasModuleType: Bool, hasPluginType: Bool
|
|
248
|
+
) -> Bool {
|
|
249
|
+
if title.hasPrefix("$:/") { return true }
|
|
250
|
+
if type == "application/json" && hasPluginType { return true }
|
|
251
|
+
if hasModuleType { return true }
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-tiddlywiki-filesystem-android-external-storage",
|
|
3
|
-
"version": "2.2.
|
|
4
|
-
"description": "Expo native module for TidGi-Mobile:
|
|
3
|
+
"version": "2.2.13",
|
|
4
|
+
"description": "Expo native module for TidGi-Mobile: filesystem I/O + TiddlyWiki .tid/.meta/.json batch parsing + git status in Kotlin (Android) and Swift (iOS)",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
7
7
|
"plugin": "app.plugin.js",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"files": [
|
|
40
40
|
"build",
|
|
41
41
|
"android",
|
|
42
|
+
"ios",
|
|
42
43
|
"expo-module.config.json",
|
|
43
44
|
"src",
|
|
44
45
|
"plugin.js",
|
package/src/index.ts
CHANGED
|
@@ -19,8 +19,8 @@ let _module: IExternalStorageModule | undefined;
|
|
|
19
19
|
*/
|
|
20
20
|
function getNativeModule(): IExternalStorageModule {
|
|
21
21
|
if (_module) return _module;
|
|
22
|
-
if (Platform.OS !== 'android') {
|
|
23
|
-
throw new Error('ExternalStorage native module is only available on Android');
|
|
22
|
+
if (Platform.OS !== 'android' && Platform.OS !== 'ios') {
|
|
23
|
+
throw new Error('ExternalStorage native module is only available on Android and iOS');
|
|
24
24
|
}
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
26
|
const { requireNativeModule } = require('expo-modules-core') as { requireNativeModule: (name: string) => IExternalStorageModule };
|