mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-09 23:47:27 +00:00
Some checks are pending
Keyman Build Summary / Summarize build status checks (push) Waiting to run
639 lines
24 KiB
Swift
639 lines
24 KiB
Swift
/*
|
|
* Keyman is copyright (C) SIL Global. MIT License.
|
|
*
|
|
* Created by Shawn Schantz on 2026-04-02
|
|
*
|
|
* Class that exposes all settings information to the Keyman Configuration app
|
|
* Provides a place for the config app to bind directly to the settings
|
|
* and update when changes are made
|
|
*
|
|
* The Settings consist of two types of data:
|
|
* - Keyman packages that have been installed on disk in the Group Containers directory
|
|
* - Some lightweight preferences that are stored in the macOS UserDefaults Database
|
|
*
|
|
* Both the packages and the defaults are in a shared location that can be accessed by
|
|
* the Keyman config app and the Keyman input method.
|
|
*
|
|
* This class accesses the two types of data through PackageRepository and DefaultsRepository
|
|
* and publishes the combined data to the UI through the `installedPackages` array.
|
|
*
|
|
* All installed packages are display in the configuration app.
|
|
* Only installed packages that are enabled appear in the Keyman menu presented by the input method.
|
|
*/
|
|
|
|
import Foundation
|
|
import Combine
|
|
import ZIPFoundation
|
|
import OSLog
|
|
import Sentry
|
|
|
|
public enum InstallPackageError: LocalizedError {
|
|
case packageInstallationAlreadyInProgress
|
|
case fontCopyError
|
|
case fontRegistrationError
|
|
case internalError // due to invalid state, should never occur
|
|
|
|
private var packageBundle: LocalizedStringResource.BundleDescription {
|
|
.atURL(Bundle.module.bundleURL)
|
|
}
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .packageInstallationAlreadyInProgress:
|
|
let resource = LocalizedStringResource(
|
|
"installation.in.progress",
|
|
defaultValue: "A package installation is already in progress.",
|
|
bundle: packageBundle)
|
|
return String(localized: resource)
|
|
case .fontCopyError:
|
|
let resource = LocalizedStringResource(
|
|
"font.copy.error",
|
|
defaultValue: "There was an error copying the font.",
|
|
bundle: packageBundle)
|
|
return String(localized: resource)
|
|
case .fontRegistrationError:
|
|
let resource = LocalizedStringResource(
|
|
"font.registration.error",
|
|
defaultValue: "There was an error registering the font.",
|
|
bundle: packageBundle)
|
|
return String(localized: resource)
|
|
case .internalError:
|
|
let resource = LocalizedStringResource(
|
|
"error.internal",
|
|
defaultValue: "An internal error occurred.",
|
|
bundle: packageBundle)
|
|
return String(localized: resource)
|
|
}
|
|
}
|
|
}
|
|
|
|
// distributed notifications
|
|
public extension Notification.Name {
|
|
// sent from input method, received by InstallationCheck
|
|
static let accessibilityStateResponse = Notification.Name("com.keyman.accessibility.state")
|
|
// sent from config app (DefaultsRepository), received by input method
|
|
static let keyboardsChanged = Notification.Name("com.keyman.keyboards.changed")
|
|
// sent from config app (InputMethodUtil), received by input method
|
|
static let accessibilityCheckRequest = Notification.Name("com.keyman.accessibility.check.request")
|
|
// sent from config app (InputMethodUtil), received by input method
|
|
static let accessibilityRequest = Notification.Name("com.keyman.accessibility.request")
|
|
}
|
|
|
|
// in-app notifications
|
|
public extension Notification.Name {
|
|
// sent from InstallationContainer to SettingsContainer so it can reload packages
|
|
static let dataMigrated = Notification.Name("com.keyman.data.migrated")
|
|
}
|
|
|
|
public enum DropKmpError: LocalizedError {
|
|
case invalidFileType(String)
|
|
case exceededFileDropLimit
|
|
|
|
private var packageBundle: LocalizedStringResource.BundleDescription {
|
|
.atURL(Bundle.module.bundleURL)
|
|
}
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .invalidFileType(let fileName):
|
|
let resource = LocalizedStringResource(
|
|
"non.kmp.file",
|
|
defaultValue: "The file \(fileName) is not a .KMP file.",
|
|
bundle: packageBundle)
|
|
return String(localized: resource)
|
|
case .exceededFileDropLimit:
|
|
let resource = LocalizedStringResource(
|
|
"exceeded.file.drop.limit",
|
|
defaultValue: "Only a single .KMP file can be installed at a time.",
|
|
bundle: packageBundle)
|
|
return String(localized: resource)
|
|
}
|
|
}
|
|
}
|
|
|
|
package let kmpFileExtension = ".kmp"
|
|
package let kmpFileExtensionWithoutDot = "kmp"
|
|
|
|
@MainActor // run on the main actor since data is published directly to the UI
|
|
public class SettingsContainer : ObservableObject {
|
|
// installed packages are loaded from disk, each package may contain one or more keyboard
|
|
fileprivate var installedPackages: [KeymanPackage] {
|
|
didSet {
|
|
// whenever this array is modified, update the arrays that are derived from it
|
|
self.updatePackageArrays()
|
|
}
|
|
}
|
|
|
|
// Maintain two arrays for use by configuration views.
|
|
// One is for packages with single keyboards and one for packages with multiple keyboards.
|
|
// Each array is sorted alphabetically by package name.
|
|
// These arrays are updated by a property observer on installedPackages.
|
|
// (Consider installedPackages as the source of truth and these arrays for presentation purposes.)
|
|
@Published public private(set) var singleKeyboardPackages: [KeymanPackage]
|
|
@Published public private(set) var multiKeyboardPackages: [KeymanPackage]
|
|
|
|
// when a new package is being installed, it is tracked here
|
|
fileprivate var packageInstall: PackageInstallHelper? = nil
|
|
|
|
fileprivate let packageRepository: PackageRepo
|
|
fileprivate let defaultsRepository: DefaultsRepo
|
|
|
|
// the selected keyboard is stored in the UserDefaults
|
|
// not indicated in the Config app but this could change
|
|
fileprivate var selectedKeyboard: String
|
|
|
|
private let keyboardSearchPrefix = "https://keyman.com/go/macos/14.0/download-keyboards"
|
|
|
|
public var keyboardSearchUrl: URL {
|
|
var searchUrl = URL(string: keyboardSearchPrefix)!
|
|
|
|
let versionQueryItem = URLQueryItem(name:"version", value: ConfigAppUtil.configAppVersion())
|
|
searchUrl.append(queryItems:[versionQueryItem])
|
|
|
|
if let languageString = self.getPreferredLangauge() {
|
|
let languageQueryItem = URLQueryItem(name:"lang", value: languageString)
|
|
searchUrl.append(queryItems:[languageQueryItem])
|
|
}
|
|
Logger.setup.info("full searchUrl: \(searchUrl.absoluteString, privacy: .public)")
|
|
LogUtil.infoBreadcrumb("full searchUrl: \(searchUrl.absoluteString)", category: .setup)
|
|
return searchUrl
|
|
}
|
|
|
|
/**
|
|
* returns the preferred language based on the user's preferences set in System Settings
|
|
*/
|
|
private func getPreferredLangauge() -> String? {
|
|
// returns an array of language identifiers ordered by the user's preference
|
|
// this is independent of what the languages the app actually supports
|
|
let systemLanguages = Locale.preferredLanguages
|
|
|
|
var primaryLanguage: String?
|
|
if let language = systemLanguages.first {
|
|
primaryLanguage = language
|
|
Logger.setup.info("primary system language: \(language, privacy: .public)")
|
|
LogUtil.infoBreadcrumb("primary system language: \(language)", category: .setup)
|
|
}
|
|
|
|
return primaryLanguage
|
|
}
|
|
|
|
public init() {
|
|
// initialize arrays before loading packages
|
|
self.singleKeyboardPackages = []
|
|
self.multiKeyboardPackages = []
|
|
self.installedPackages = []
|
|
|
|
// create the package repository, gaining access to the app group container directory
|
|
do {
|
|
try self.packageRepository = PackageRepository()
|
|
Logger.data.log("Found documents group container")
|
|
} catch KeymanPathError.groupContainerNotFound {
|
|
Logger.data.error("Document group container not found")
|
|
fatalError("Document group container not found.")
|
|
} catch {
|
|
Logger.data.error("Unable to access documents in group container, error \(error as NSError, privacy: .public)")
|
|
fatalError("Unable to access documents in group container.")
|
|
}
|
|
|
|
// create the settings repository, gaining access to the app group UserDefaults
|
|
do {
|
|
try self.defaultsRepository = DefaultsRepository(suiteName: InputMethodUtil.groupId)
|
|
Logger.data.log("Found defaults group container")
|
|
} catch UserDefaultsError.unknownSuite {
|
|
Logger.data.error("Defaults group container not found")
|
|
fatalError("Defaults group container not found.")
|
|
} catch {
|
|
Logger.data.error("Unable to access defaults in group container, error \(error as NSError, privacy: .public)")
|
|
fatalError("Unable to access defaults in group container: \(error.localizedDescription).")
|
|
}
|
|
|
|
self.selectedKeyboard = self.defaultsRepository.readSelectedKeyboard()
|
|
|
|
// load all the installed packages from disk
|
|
// though we are in the initializer, didset will be called to update
|
|
// the two dependent package arrays because of the call to this helper method
|
|
self.loadPackages()
|
|
|
|
// next, apply the settings to the packages
|
|
// this mainly consists of marking them as enabled or not
|
|
self.applyUserDefaultsToInstalledPackages()
|
|
|
|
self.registerObservers()
|
|
}
|
|
|
|
/**
|
|
* initializer only for use with unit tests, allows injection of stubs for `DefaultsRepo` and `PackageRepo`
|
|
*/
|
|
init(defaultsRepo: DefaultsRepo, packageRepo: PackageRepo) {
|
|
self.defaultsRepository = defaultsRepo
|
|
self.packageRepository = packageRepo
|
|
self.selectedKeyboard = self.defaultsRepository.readSelectedKeyboard()
|
|
|
|
self.singleKeyboardPackages = []
|
|
self.multiKeyboardPackages = []
|
|
self.installedPackages = []
|
|
}
|
|
|
|
/**
|
|
* register observers to receive
|
|
*/
|
|
func registerObservers() {
|
|
NotificationCenter.default.addObserver(self, selector: #selector(self.reloadPackages), name: .dataMigrated, object: nil)
|
|
}
|
|
|
|
/**
|
|
* Refresh the packages array and apply settings.
|
|
* This should only be needed after a migration and could be removed if
|
|
* the migration were made earlier.
|
|
*/
|
|
@objc public func reloadPackages() {
|
|
self.loadPackages()
|
|
self.applyUserDefaultsToInstalledPackages()
|
|
}
|
|
|
|
/**
|
|
* Whenever the installedPackages array changes, recreate the two subarrays
|
|
*/
|
|
public func updatePackageArrays() {
|
|
self.singleKeyboardPackages = []
|
|
self.multiKeyboardPackages = []
|
|
|
|
// divide the installedPackages array into two separate arrays based on whether they have one or more keyboards
|
|
let partitionedPackages = self.installedPackages.reduce(into: (single: [KeymanPackage](), multiple: [KeymanPackage]())) { result, element in
|
|
if element.keyboards.count == 1 {
|
|
result.single.append(element)
|
|
} else if element.keyboards.count > 1 {
|
|
result.multiple.append(element)
|
|
}
|
|
}
|
|
|
|
// sort subarrays alphabetically, without regard to case, using the 'packageName' property
|
|
self.singleKeyboardPackages = partitionedPackages.single.sorted { $0.packageName.caseInsensitiveCompare($1.packageName) == .orderedAscending }
|
|
self.multiKeyboardPackages = partitionedPackages.multiple.sorted { $0.packageName.caseInsensitiveCompare($1.packageName) == .orderedAscending }
|
|
}
|
|
|
|
|
|
/**
|
|
* Build the URL necessary to download the specified package from the Keyman website
|
|
*/
|
|
public func buildDownloadPackageUrl(for packageId: String) -> URL? {
|
|
let urlString = "https://keyman.com/go/package/download/\(packageId)?platform=macos&tier=\(ConfigAppUtil.appTier)"
|
|
|
|
guard let downloadPackageUrl = URL(string: urlString) else {
|
|
return nil
|
|
}
|
|
|
|
return downloadPackageUrl
|
|
}
|
|
|
|
/**
|
|
* Called when user chooses to cancel downgrade of package
|
|
*/
|
|
public func userCanceledPackageInstallation() {
|
|
Logger.data.log("User cancelled package installation")
|
|
self.packageInstall?.cleanupFailedInstallation()
|
|
|
|
self.packageInstall = nil
|
|
}
|
|
|
|
/**
|
|
* Called when user chooses to cancel downgrade of package
|
|
*/
|
|
public func packageInstallationFailed() {
|
|
Logger.data.log("Package installation failed")
|
|
self.packageInstall?.cleanupFailedInstallation()
|
|
|
|
self.packageInstall = nil
|
|
}
|
|
|
|
/**
|
|
* for each enabled keyboard in the package being installed, add it to the enabled keyboards set and save it in the UserDefaults
|
|
*/
|
|
func addEnabledKeyboards(for installedPackage: KeymanPackage) {
|
|
let currentlyEnabledKeyboards = self.defaultsRepository.readEnabledKeyboards()
|
|
var updatedEnabledKeyboards = currentlyEnabledKeyboards
|
|
|
|
// set enabled flag if the keyboard is contained in the set of enabledKeyboards
|
|
installedPackage.keyboards.forEach {
|
|
updatedEnabledKeyboards.insert($0.keyboardKey)
|
|
}
|
|
|
|
if updatedEnabledKeyboards != currentlyEnabledKeyboards {
|
|
self.defaultsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(updatedEnabledKeyboards))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* find the installed package with the specified UUID
|
|
*/
|
|
public func findInstalledPackage(with id: UUID) -> KeymanPackage? {
|
|
guard let package = self.installedPackages.first(where: { $0.id == id }) else {
|
|
Logger.setup.error("error: could not find package with UUID: \(id)")
|
|
LogUtil.errorBreadcrumb("error: could not find package with UUID: \(id)", category: .setup)
|
|
return nil
|
|
}
|
|
|
|
return package
|
|
}
|
|
|
|
/**
|
|
* remove/uninstall the package with the specified UUID
|
|
*/
|
|
public func removeInstalledPackage(with id: UUID) {
|
|
if let package = findInstalledPackage(with: id) {
|
|
self.removeInstalledPackage(package: package)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* remove the installed package
|
|
*/
|
|
func removeInstalledPackage(package: KeymanPackage) {
|
|
// will removing this package cause the removal of any enabled keyboards?
|
|
let removingEnabledKeyboards = !package.getEnabledKeyboardsKeys().isEmpty
|
|
|
|
// delete package from disk
|
|
self.packageRepository.deletePackage(package: package)
|
|
|
|
// remove package from installed packages list
|
|
if let index = self.installedPackages.firstIndex(where: { $0.id == package.id }) {
|
|
self.installedPackages.remove(at: index)
|
|
}
|
|
|
|
// if we removed any enabled keyboards, then update settings
|
|
if removingEnabledKeyboards {
|
|
self.saveKeyboardState()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* returns true if the keyboard is enabled
|
|
* when enabled, the keyboard appears in the Keyman sub menu in the mac
|
|
*/
|
|
public func isKeyboardEnabled(packageId: UUID, keyboardKey: String) -> Bool {
|
|
guard let package = self.findInstalledPackage(with: packageId) else {
|
|
Logger.setup.error("isKeyboardEnabled, not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey, privacy: .public)")
|
|
LogUtil.errorBreadcrumb("isKeyboardEnabled, not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey)", category: .setup)
|
|
return false
|
|
}
|
|
|
|
let enabled = package.isKeyboardEnabled(keyboardKey: keyboardKey)
|
|
return enabled
|
|
}
|
|
|
|
/**
|
|
* enable or disable the keyboard
|
|
*/
|
|
public func setKeyboardEnabled(packageId: UUID, keyboardKey: String, enabled: Bool) {
|
|
guard let package = self.findInstalledPackage(with: packageId) else {
|
|
Logger.setup.error("setKeyboardEnabled, could not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey, privacy: .public)")
|
|
LogUtil.errorBreadcrumb("setKeyboardEnabled, could not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey)", category: .setup)
|
|
return
|
|
}
|
|
|
|
Logger.setup.info("setKeyboardEnabled for \(keyboardKey, privacy: .public) setting to \(enabled)")
|
|
LogUtil.infoBreadcrumb("setKeyboardEnabled for \(keyboardKey)", category: .setup)
|
|
package.enableKeyboard(keyboardKey: keyboardKey, enabled: enabled)
|
|
|
|
// update persisted state in UserDefaults enabledKeyboards array
|
|
self.saveKeyboardState()
|
|
}
|
|
|
|
/**
|
|
* save the keyboard state in the UserDefaults
|
|
*/
|
|
func saveKeyboardState() {
|
|
let enabledKeyboards = self.getEnabledKeyboardKeys()
|
|
self.defaultsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(enabledKeyboards))
|
|
}
|
|
|
|
/**
|
|
* read the Keyman packages from the group container directory and store in the installedPackages array
|
|
*/
|
|
func loadPackages() {
|
|
self.installedPackages = self.packageRepository.loadAllPackages()
|
|
}
|
|
|
|
/**
|
|
* returns set containing the keyboards settings keys for all installed keyboards
|
|
*/
|
|
func getInstalledKeyboardKeys() -> Set<String> {
|
|
var settingsKeys = Set<String>()
|
|
|
|
// loop through all the installed packages and for each of the package's keyboards,
|
|
// insert the settings key for the keyboard
|
|
self.installedPackages.forEach { $0.keyboards.forEach
|
|
{settingsKeys.insert($0.keyboardKey)}
|
|
}
|
|
|
|
return settingsKeys
|
|
}
|
|
|
|
/**
|
|
* returns set containing the keyboards settings keys for all installed keyboards which are enabled
|
|
*/
|
|
func getEnabledKeyboardKeys() -> Set<String> {
|
|
var settingsKeys = Set<String>()
|
|
|
|
// loop through all the installed packages and for each of the package's keyboards,
|
|
// insert the settings key for every enabled keyboard
|
|
self.installedPackages.forEach { $0.keyboards.forEach {
|
|
if ($0.enabled) {
|
|
settingsKeys.insert($0.keyboardKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
return settingsKeys
|
|
}
|
|
|
|
/**
|
|
* ensure that UserDefaults are consistent with the installed packages
|
|
* remove any UserDefaults for which no installed package exists
|
|
*/
|
|
func validateUserDefaults() {
|
|
let installedKeyboardKeys = self.getInstalledKeyboardKeys()
|
|
let enabledKeyboardKeys = self.defaultsRepository.readEnabledKeyboards()
|
|
|
|
if (enabledKeyboardKeys.isSubset(of: installedKeyboardKeys)) {
|
|
Logger.setup.info("only installed keyboards are listed as enabled: no need to update defaults")
|
|
} else {
|
|
Logger.setup.info("enabled keyboards list contains uninstalled keyboards: align with enabled keyboards list")
|
|
let installedEnabledKeyboardKeys = enabledKeyboardKeys.intersection(installedKeyboardKeys)
|
|
self.defaultsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(installedEnabledKeyboardKeys))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* apply the state from the current UserDefaults to the installed packages
|
|
*/
|
|
func applyUserDefaultsToInstalledPackages() {
|
|
self.validateUserDefaults()
|
|
|
|
let enabledKeyboards = self.defaultsRepository.readEnabledKeyboards()
|
|
|
|
// set enabled flag if the keyboard is contained in the set of enabledKeyboards
|
|
self.installedPackages.forEach { $0.keyboards.forEach
|
|
{
|
|
$0.enabled = enabledKeyboards.contains($0.keyboardKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Package Download and Installation
|
|
|
|
/**
|
|
* check whether an installation is already in progress
|
|
*/
|
|
public func isInstallationInProgress() -> Bool {
|
|
return self.packageInstall != nil
|
|
}
|
|
|
|
/**
|
|
* Called by the WebView DownloadCoordinator before initiating a package download.
|
|
* Returns a PackageInstallHelper instance to manage the state of the package being downloaded with the specified name.
|
|
*/
|
|
public func initiateKmpFileDownload(kmpFilename: String) throws -> PackageInstallHelper? {
|
|
|
|
guard !self.isInstallationInProgress() else {
|
|
throw InstallPackageError.packageInstallationAlreadyInProgress
|
|
}
|
|
|
|
if let helper = self.preparePackageDownload(kmpFilename: kmpFilename) {
|
|
self.packageInstall = helper
|
|
}
|
|
|
|
return self.packageInstall
|
|
}
|
|
|
|
/**
|
|
* Creates a PackageInstallHelper instance to manage the state of the package being downloaded with the specified name.
|
|
*/
|
|
func preparePackageDownload(kmpFilename: String) -> PackageInstallHelper? {
|
|
return PackageInstallHelper(filename: kmpFilename, packageRepo: self.packageRepository, installedPackages: self.installedPackages, isDownload: true)
|
|
}
|
|
|
|
/**
|
|
* Called by the WebView DownloadCoordinator after the download is complete.
|
|
* Delegates to the PackageInstallHelper instance to decide whether the package should be installed.
|
|
*/
|
|
public func packageDownloadComplete(kmpFileUrl: URL) throws {
|
|
Logger.setup.info("packageDownloadComplete \(kmpFileUrl.cleanUrlPath(), privacy: .public)")
|
|
LogUtil.infoBreadcrumb("packageDownloadComplete \(kmpFileUrl.cleanUrlPath())", category: .setup)
|
|
|
|
do {
|
|
try self.packageInstall?.prepareToInstall(for: kmpFileUrl)
|
|
} catch {
|
|
// clear failed download
|
|
self.packageInstall = nil
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The package is approved for installation, so add it to the package list and update the UserDefaults for enabled keyboards
|
|
*/
|
|
func addInstalledPackage() {
|
|
if let package = self.packageInstall?.packageToInstall {
|
|
self.installedPackages.append(package)
|
|
self.addEnabledKeyboards(for: package)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The package is approved for installation, so replace the package of the same name in the package list.
|
|
* Also update the UserDefaults for enabled keyboards because the new package is enabled by default, and the existing may be disabled
|
|
*/
|
|
func replaceInstalledPackage() {
|
|
if let package = self.packageInstall?.packageToInstall {
|
|
// find the existing package with the same name in the installedPackages array and replace it
|
|
// (we cannot use the id for this search, as the ids are unique)
|
|
if let index = self.installedPackages.firstIndex(where: { $0.packageName == package.packageName }) {
|
|
self.installedPackages[index] = package
|
|
self.addEnabledKeyboards(for: package)
|
|
} else {
|
|
Logger.setup.error("Error: package '\(package.packageName, privacy: .public)' not found for replacement")
|
|
LogUtil.errorBreadcrumb("Error: package '\(package.packageName)' not found for replacement", category: .setup)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Drag and drop Package Installation
|
|
|
|
/**
|
|
* Begin installation of a package from a KMP file.
|
|
* Called when a .KMP file is dropped on the Configuration view
|
|
*/
|
|
public func initiateKmpFileInstallation(at fileLocation: URL) throws -> PackageInstallHelper? {
|
|
guard !self.isInstallationInProgress() else {
|
|
throw InstallPackageError.packageInstallationAlreadyInProgress
|
|
}
|
|
|
|
// validate the URL of the KMP file
|
|
try self.validateDroppedFile(from: fileLocation)
|
|
|
|
let kmpFilename = fileLocation.lastPathComponent
|
|
if let helper = self.preparePackageDrop(kmpFilename: kmpFilename) {
|
|
self.packageInstall = helper
|
|
do {
|
|
try helper.prepareToInstall(for: fileLocation)
|
|
} catch {
|
|
// clear failed download
|
|
self.packageInstall = nil
|
|
throw error
|
|
}
|
|
}
|
|
|
|
return self.packageInstall
|
|
}
|
|
|
|
/**
|
|
* Install the package and add it to the installedPackages array and UserDefaults
|
|
*/
|
|
public func installPackage() throws {
|
|
if let install = self.packageInstall {
|
|
do {
|
|
try install.installPackage()
|
|
} catch {
|
|
self.packageInstall?.cleanupFailedInstallation()
|
|
self.packageInstall = nil
|
|
throw error
|
|
}
|
|
commitPackageInstall()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the data model for the installed package.
|
|
*/
|
|
func commitPackageInstall() {
|
|
if let install = self.packageInstall {
|
|
|
|
guard let installationType = install.packageInstallationType else { return }
|
|
|
|
switch installationType {
|
|
case .newPackage:
|
|
self.addInstalledPackage()
|
|
case .replaceSameVersionPackage, .replaceNewerPackage, .replaceOlderPackage:
|
|
self.replaceInstalledPackage()
|
|
}
|
|
}
|
|
|
|
self.packageInstall = nil
|
|
}
|
|
|
|
/**
|
|
* Creates a PackageInstallHelper instance to manage the state of the package being installed with the specified name.
|
|
*/
|
|
func preparePackageDrop(kmpFilename: String) -> PackageInstallHelper? {
|
|
return PackageInstallHelper(filename: kmpFilename, packageRepo: self.packageRepository, installedPackages: self.installedPackages, isDownload: false)
|
|
}
|
|
|
|
/**
|
|
* Validate the URL for the file we are dropping
|
|
*/
|
|
func validateDroppedFile(from fileLocation: URL) throws {
|
|
// if the file does not end with .kmp, reject it
|
|
if fileLocation.pathExtension.lowercased() != kmpFileExtensionWithoutDot {
|
|
throw DropKmpError.invalidFileType(fileLocation.lastPathComponent)
|
|
}
|
|
}
|
|
}
|