Skip to content

Commit 0b0bcd1

Browse files
committed
Add plugin import and delete to the plugins screen
1 parent 82a462f commit 0b0bcd1

1 file changed

Lines changed: 70 additions & 2 deletions

File tree

Sources/BluetoothExplorerUI/PluginsView.swift

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,26 @@
22
// PluginsView.swift
33
// BluetoothExplorerUI
44
//
5-
// Lists loaded parser plugins with enable/disable toggles and load-error surfacing.
5+
// Manages parser plugins: enable/disable, import from a file, delete, and surface load errors.
6+
// Bundled plugins are copied into the app's Documents directory on first launch, so everything
7+
// listed here lives in one place the user can inspect.
68
//
79

810
import SwiftUI
11+
#if canImport(UniformTypeIdentifiers)
12+
import UniformTypeIdentifiers
13+
#endif
914
import BluetoothExplorerModel
1015

1116
public struct PluginsView: View {
1217

1318
@Environment(Store.self)
1419
var store: Store
1520

21+
// Not `private`: Skip's bridging requires @State properties to be at least internal.
22+
@State var isImporting = false
23+
@State var importError: String?
24+
1625
public init() {}
1726

1827
public var body: some View {
@@ -21,15 +30,74 @@ public struct PluginsView: View {
2130
ForEach(store.pluginManager.plugins) { state in
2231
row(for: state)
2332
}
33+
.onDelete { offsets in
34+
delete(at: offsets)
35+
}
2436
} header: {
2537
Text("Parsers")
2638
} footer: {
27-
Text("Plugins decode advertisement and characteristic values. Built-in parsers are native; others are WebAssembly modules.")
39+
Text("Plugins decode advertisement and characteristic values. Built-in parsers are native; others are WebAssembly modules stored in the app's Documents folder.")
40+
}
41+
42+
if let error = importError {
43+
Section {
44+
Text(verbatim: error)
45+
.font(.caption)
46+
.foregroundColor(.red)
47+
} header: {
48+
Text("Import Failed")
49+
}
2850
}
2951
}
3052
.navigationTitle("Plugins")
53+
#if !os(Android)
54+
.toolbar {
55+
Button {
56+
importError = nil
57+
isImporting = true
58+
} label: {
59+
Label("Import", systemImage: "square.and.arrow.down")
60+
}
61+
}
62+
.fileImporter(
63+
isPresented: $isImporting,
64+
allowedContentTypes: [.json],
65+
allowsMultipleSelection: false
66+
) { result in
67+
handleImport(result)
68+
}
69+
#endif
3170
}
3271

72+
// MARK: Actions
73+
74+
private func handleImport(_ result: Result<[URL], Error>) {
75+
switch result {
76+
case let .success(urls):
77+
guard let url = urls.first else { return }
78+
switch store.pluginManager.importPlugin(manifestURL: url) {
79+
case .success:
80+
importError = nil
81+
case let .failure(error):
82+
importError = error.message
83+
}
84+
case let .failure(error):
85+
importError = "\(error)"
86+
}
87+
}
88+
89+
private func delete(at offsets: IndexSet) {
90+
let plugins = store.pluginManager.plugins
91+
for index in offsets where index < plugins.count {
92+
let state = plugins[index]
93+
// Native parsers are compiled in; there is nothing on disk to remove.
94+
guard state.source != .native else { continue }
95+
store.pluginManager.removePlugin(id: state.id)
96+
}
97+
}
98+
99+
// MARK: Rows
100+
33101
@ViewBuilder
34102
private func row(for state: PluginManager.PluginState) -> some View {
35103
VStack(alignment: .leading, spacing: 3) {

0 commit comments

Comments
 (0)