Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions TablePro/Core/Plugins/PluginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ final class PluginManager {

private(set) var plugins: [PluginEntry] = []

private(set) var isInstalling = false

private(set) var needsRestart = false

private(set) var driverPlugins: [String: any DriverPlugin] = [:]
Expand Down Expand Up @@ -182,6 +184,14 @@ final class PluginManager {
}
}

private func replaceExistingPlugin(bundleId: String) {
guard let existingIndex = plugins.firstIndex(where: { $0.id == bundleId }) else { return }
// Order matters: unregisterCapabilities reads from `plugins` to find the principal class
unregisterCapabilities(pluginId: bundleId)
plugins[existingIndex].bundle.unload()
plugins.remove(at: existingIndex)
}

private func unregisterCapabilities(pluginId: String) {
driverPlugins = driverPlugins.filter { _, value in
guard let entry = plugins.first(where: { $0.id == pluginId }) else { return true }
Expand Down Expand Up @@ -232,14 +242,20 @@ final class PluginManager {
// MARK: - Install / Uninstall

func installPlugin(from url: URL) async throws -> PluginEntry {
guard !isInstalling else {
throw PluginError.installFailed("Another plugin installation is already in progress")
}
isInstalling = true
defer { isInstalling = false }

if url.pathExtension == "tableplugin" {
return try await installBundle(from: url)
return try installBundle(from: url)
} else {
return try await installFromZip(from: url)
}
}

private func installBundle(from url: URL) async throws -> PluginEntry {
private func installBundle(from url: URL) throws -> PluginEntry {
guard let sourceBundle = Bundle(url: url) else {
throw PluginError.invalidBundle("Cannot create bundle from \(url.lastPathComponent)")
}
Expand All @@ -251,19 +267,17 @@ final class PluginManager {
throw PluginError.pluginConflict(existingName: existing.name)
}

if let existingIndex = plugins.firstIndex(where: { $0.id == newBundleId }) {
unregisterCapabilities(pluginId: newBundleId)
plugins[existingIndex].bundle.unload()
plugins.remove(at: existingIndex)
}
replaceExistingPlugin(bundleId: newBundleId)

let fm = FileManager.default
let destURL = userPluginsDir.appendingPathComponent(url.lastPathComponent)

if fm.fileExists(atPath: destURL.path) {
try fm.removeItem(at: destURL)
if url.standardizedFileURL != destURL.standardizedFileURL {
if fm.fileExists(atPath: destURL.path) {
try fm.removeItem(at: destURL)
}
try fm.copyItem(at: url, to: destURL)
}
try fm.copyItem(at: url, to: destURL)

let entry = try loadPlugin(at: destURL, source: .userInstalled)

Expand Down Expand Up @@ -321,6 +335,8 @@ final class PluginManager {
throw PluginError.pluginConflict(existingName: existing.name)
}

replaceExistingPlugin(bundleId: newBundleId)

let destURL = userPluginsDir.appendingPathComponent(extracted.lastPathComponent)

if fm.fileExists(atPath: destURL.path) {
Expand Down
4 changes: 4 additions & 0 deletions TablePro/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
</array>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>tableplugin</string>
</array>
<key>CFBundleTypeName</key>
<string>TablePro Plugin</string>
<key>CFBundleTypeRole</key>
Expand Down
7 changes: 2 additions & 5 deletions TablePro/Views/Settings/Plugins/InstalledPluginsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ struct InstalledPluginsView: View {
private let pluginManager = PluginManager.shared

@State private var selectedPluginId: String?
@State private var isInstalling = false
@State private var showErrorAlert = false
@State private var errorAlertTitle = ""
@State private var errorAlertMessage = ""
Expand Down Expand Up @@ -51,9 +50,9 @@ struct InstalledPluginsView: View {
Button("Install from File...") {
installFromFile()
}
.disabled(isInstalling)
.disabled(pluginManager.isInstalling)

if isInstalling {
if pluginManager.isInstalling {
ProgressView()
.controlSize(.small)
}
Expand Down Expand Up @@ -205,9 +204,7 @@ struct InstalledPluginsView: View {
}

private func installPlugin(from url: URL) {
isInstalling = true
Task {
defer { isInstalling = false }
do {
let entry = try await pluginManager.installPlugin(from: url)
selectedPluginId = entry.id
Expand Down