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
58 changes: 0 additions & 58 deletions TablePro/Core/Plugins/MissingDriverPluginPrompt.swift

This file was deleted.

43 changes: 0 additions & 43 deletions TablePro/Core/Plugins/MissingExportFormatPrompt.swift

This file was deleted.

2 changes: 1 addition & 1 deletion TablePro/Core/Plugins/PluginError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ enum PluginError: LocalizedError {
case .downloadFailed(let reason):
return String(format: String(localized: "Plugin download failed: %@"), reason)
case .pluginNotInstalled(let databaseType):
return String(format: String(localized: "The %@ plugin is not installed. You can download it from the plugin marketplace."), databaseType)
return String(format: String(localized: "The %@ plugin is not installed."), databaseType)
case .pluginUpdateUnavailable(let reason):
return reason
case .incompatibleWithCurrentApp(let minimumRequired):
Expand Down
38 changes: 7 additions & 31 deletions TablePro/Core/Plugins/PluginInstallProgressPresenter.swift
Original file line number Diff line number Diff line change
@@ -1,34 +1,19 @@
//
// PluginInstallProgressPresenter.swift
// TablePro
//

import AppKit

/// The progress of a plugin download the user is waiting on, shown while it runs.
///
/// Every caller of `installMissingPlugin` used to discard the fraction it publishes, so pressing
/// Install closed the alert and left the app looking hung for the length of a network download.
/// The HIG asks for progress on anything past a couple of seconds, and a registry ZIP is reliably
/// past it.
///
/// A panel rather than an `NSAlert`, for two reasons. `AlertHelper` runs an alert application-modal
/// when no window qualifies, which is exactly the Finder-open case this exists for, and a modal run
/// loop cannot be updated from the `await` that is driving it. And an alert with no button is not
/// what `NSAlert` is for: this is a progress report, not a question.
// A panel, not an NSAlert: with no window an alert runs app-modal, and the await driving it cannot update it.
@MainActor
internal final class PluginInstallProgressPresenter {
private var panel: NSPanel?
private var sheetParent: NSWindow?
private let indicator = NSProgressIndicator()
private let label = NSTextField(labelWithString: "")

internal init() {}
private init() {}

// Ended before the caller presents a failure, since a second sheet on the window would queue.
internal static func run(
title: String,
window: NSWindow? = nil,
window: NSWindow?,
_ install: (_ progress: @escaping @MainActor @Sendable (Double) -> Void) async throws -> Void
) async throws {
let presenter = PluginInstallProgressPresenter()
Expand All @@ -37,9 +22,7 @@ internal final class PluginInstallProgressPresenter {
try await install { fraction in presenter.update(fraction: fraction) }
}

/// Presented as a sheet on the window the user was working in, and as a free-standing panel
/// when there is none, which is how a Finder open before any window exists reaches the screen.
internal func begin(title: String, window: NSWindow? = nil) {
private func begin(title: String, window: NSWindow?) {
guard panel == nil else { return }
label.stringValue = title
let panel = makePanel()
Expand All @@ -54,13 +37,8 @@ internal final class PluginInstallProgressPresenter {
parent.beginSheet(panel)
}

/// The first fraction that has actually moved is what turns the bar determinate. A zero is
/// published before the first byte arrives, and a server that sends no `Content-Length`
/// publishes nothing after it until the whole file is down, so adopting it would park a
/// determinate bar at 0% for the entire download: the wait this panel exists to explain.
/// Stopping the animation goes with the switch, or its timer runs on under a bar that is no
/// longer using it.
internal func update(fraction: Double) {
// The installer publishes 0 before the first byte, then maybe nothing until the end, so 0 stays indeterminate.
private func update(fraction: Double) {
let clamped = min(max(fraction, 0), 1)
guard clamped > 0 else { return }
if indicator.isIndeterminate {
Expand All @@ -70,7 +48,7 @@ internal final class PluginInstallProgressPresenter {
indicator.doubleValue = clamped * 100
}

internal func end() {
private func end() {
indicator.stopAnimation(nil)
guard let panel else { return }
self.panel = nil
Expand All @@ -89,8 +67,6 @@ internal final class PluginInstallProgressPresenter {
label.font = .preferredFont(forTextStyle: .body)
label.lineBreakMode = .byTruncatingMiddle

/// Starts indeterminate because the first byte has not arrived yet, and a bar sitting at
/// zero reads as stalled rather than as starting.
indicator.translatesAutoresizingMaskIntoConstraints = false
indicator.style = .bar
indicator.isIndeterminate = true
Expand Down
81 changes: 81 additions & 0 deletions TablePro/Core/Plugins/PluginInstallPrompt.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import AppKit
import os

@MainActor
internal enum PluginInstallPrompt {
nonisolated private static let logger = Logger(subsystem: "com.TablePro", category: "PluginInstallPrompt")

internal typealias Install = (_ progress: @escaping @MainActor @Sendable (Double) -> Void) async throws -> Void

internal static func ensureInstalled(_ format: InstallableExportFormat, window: NSWindow?) async -> Bool {
await confirmAndRun(
pluginName: format.displayName,
title: String(format: String(localized: "Install the %@ plugin?"), format.displayName),
message: String(localized: "TablePro writes this format with a plugin it downloads from the plugin registry."),
window: window
) { progress in
try await PluginManager.shared.installMissingExportFormat(format, progress: progress)
}
}

internal static func ensureInstalled(for type: DatabaseType, opening url: URL) async -> Bool {
// A Finder open is routed before eager-path plugins register, so asking first offers one the user has.
if !PluginManager.shared.isDriverInstalled(for: type) {
await PluginManager.shared.waitForInitialLoad()
}
guard !PluginManager.shared.isDriverInstalled(for: type) else { return true }
// A bundled driver that did not load is disabled or damaged; the connect attempt reports which.
guard type.isDownloadablePlugin else { return true }

let displayName = PluginMetadataRegistry.shared.snapshot(for: type)?.displayName ?? type.rawValue
return await confirmAndRun(
pluginName: displayName,
title: String(
format: String(localized: "Install the %@ plugin to open “%@”?"),
displayName,
url.lastPathComponent
),
message: String(localized: "TablePro reads this file with a driver it downloads from the plugin registry."),
window: nil
) { progress in
try await PluginManager.shared.installMissingPlugin(for: type, progress: progress)
}
}

internal static func run(pluginName: String, window: NSWindow?, _ install: Install) async -> Bool {
do {
try await PluginInstallProgressPresenter.run(
title: String(format: String(localized: "Downloading the %@ plugin…"), pluginName),
window: window,
install
)
logger.info("Installed the \(pluginName, privacy: .public) plugin")
return true
} catch {
logger.error("Install failed for \(pluginName, privacy: .public): \(error.publicLogShape, privacy: .public)")
AlertHelper.showErrorSheet(
title: String(localized: "Plugin Installation Failed"),
message: error.localizedDescription,
window: window
)
return false
}
}

private static func confirmAndRun(
pluginName: String,
title: String,
message: String,
window: NSWindow?,
install: Install
) async -> Bool {
let confirmed = await AlertHelper.confirm(
title: title,
message: message,
confirmButton: String(localized: "Install"),
window: window
)
guard confirmed else { return false }
return await run(pluginName: pluginName, window: window, install)
}
}
7 changes: 1 addition & 6 deletions TablePro/Core/Services/Infrastructure/TabRouter.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
//
// TabRouter.swift
// TablePro
//

import AppKit
import Foundation
import os
Expand Down Expand Up @@ -452,7 +447,7 @@ internal final class TabRouter {
return
}

guard await MissingDriverPluginPrompt.ensureInstalled(for: type, opening: url) else {
guard await PluginInstallPrompt.ensureInstalled(for: type, opening: url) else {
throw TabRouterError.userCancelled
}

Expand Down
8 changes: 8 additions & 0 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -179774,6 +179774,9 @@
}
}
}
},
"The %@ plugin is not installed." : {

},
"The %@ plugin is not installed. TablePro offers to install it on connect." : {
"extractionState" : "manual",
Expand All @@ -179788,6 +179791,7 @@
}
},
"The %@ plugin is not installed. Would you like to download it from the plugin marketplace?" : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand Down Expand Up @@ -179826,8 +179830,12 @@
}
}
}
},
"The %@ plugin is not installed. Would you like to install it?" : {

},
"The %@ plugin is not installed. You can download it from the plugin marketplace." : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand Down
Loading
Loading