diff --git a/TablePro/Core/Plugins/PluginManager.swift b/TablePro/Core/Plugins/PluginManager.swift index 96b98f5e..8fc9f41e 100644 --- a/TablePro/Core/Plugins/PluginManager.swift +++ b/TablePro/Core/Plugins/PluginManager.swift @@ -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] = [:] @@ -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 } @@ -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)") } @@ -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) @@ -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) { diff --git a/TablePro/Info.plist b/TablePro/Info.plist index c9553039..cc707540 100644 --- a/TablePro/Info.plist +++ b/TablePro/Info.plist @@ -29,6 +29,10 @@ + CFBundleTypeExtensions + + tableplugin + CFBundleTypeName TablePro Plugin CFBundleTypeRole diff --git a/TablePro/Views/Settings/Plugins/InstalledPluginsView.swift b/TablePro/Views/Settings/Plugins/InstalledPluginsView.swift index 57260e59..507dc199 100644 --- a/TablePro/Views/Settings/Plugins/InstalledPluginsView.swift +++ b/TablePro/Views/Settings/Plugins/InstalledPluginsView.swift @@ -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 = "" @@ -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) } @@ -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