diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 17f8687..a6ac845 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -101,6 +101,7 @@ CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; }; E689540325BE8C64000EBCEA /* DockProgress in Frameworks */ = {isa = PBXBuildFile; productRef = E689540225BE8C64000EBCEA /* DockProgress */; }; E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.swift */; }; + E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */; }; E872EE4E2808D4F100D3DD8B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E872EE502808D4F100D3DD8B /* Localizable.strings */; }; E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; }; E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; }; @@ -300,6 +301,7 @@ CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = ""; }; E2AFDCCA28F024D000864ADD /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = ""; }; + E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeInstallationStepDetailView.swift; sourceTree = ""; }; E856BB73291EDD3D00DC438B /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = ""; }; E872EE4F2808D4F100D3DD8B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E87AB3C42939B65E00D72F43 /* Hardware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hardware.swift; sourceTree = ""; }; @@ -623,6 +625,7 @@ children = ( CAFBDC67259A308B003DCC5A /* InfoPane.swift */, E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */, + E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */, ); path = InfoPane; sourceTree = ""; @@ -875,6 +878,7 @@ CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */, CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */, CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */, + E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */, CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */, E8D655C0288DD04700A139C2 /* SelectedActionType.swift in Sources */, 36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */, diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index 936bb6e..ae562d3 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -499,9 +499,9 @@ extension AppState { Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal) } } + func setInstallationStep(of runtime: DownloadableRuntime, to step: RuntimeInstallationStep) { DispatchQueue.main.async { - guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return } self.downloadableRuntimes[index].installState = .installing(step) diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index f8b6489..4c19820 100644 --- a/Xcodes/Backend/AppState+Runtimes.swift +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -49,9 +49,18 @@ extension AppState { Task { do { try await downloadRunTimeFull(runtime: runtime) + + DispatchQueue.main.async { + guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return } + self.downloadableRuntimes[index].installState = .installed + } } catch { Logger.appState.error("Error downloading runtime: \(error.localizedDescription)") + DispatchQueue.main.async { + self.error = error + self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription) + } } } @@ -83,6 +92,7 @@ extension AppState { let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2 Logger.appState.info("Downloading \(runtime.visibleIdentifier) with \(downloader)") + let url = try await self.downloadRuntime(for: runtime, downloader: downloader, progressChanged: { [unowned self] progress in DispatchQueue.main.async { self.setInstallationStep(of: runtime, to: .downloading(progress: progress)) @@ -90,15 +100,23 @@ extension AppState { }).async() Logger.appState.debug("Done downloading: \(url)") - //self.setInstallationStep(of: runtime, to: .downloading(progress: progress)) + DispatchQueue.main.async { + self.setInstallationStep(of: runtime, to: .installing) + } switch runtime.contentType { case .package: - try await self.installFromPackage(dmgURL: url, runtime: runtime) + // not supported yet (do we need to for old packages?) + throw "Installing via package not support - please install manually from \(url.description)" case .diskImage: try await self.installFromImage(dmgURL: url) + DispatchQueue.main.async { + self.setInstallationStep(of: runtime, to: .trashingArchive) + } + try Current.files.removeItem(at: url) } } + @MainActor func downloadRuntime(for runtime: DownloadableRuntime, downloader: Downloader, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher { // Check to see if the dmg is in the expected path in case it was downloaded but failed to install @@ -139,9 +157,9 @@ extension AppState { .setFailureType(to: Error.self) .eraseToAnyPublisher() -// return downloadXcodeWithURLSession( -// availableXcode, -// to: destination, +// return downloadRuntimeWithURLSession( +// runtime, +// to: expectedRuntimePath, // progressChanged: progressChanged // ) } @@ -163,36 +181,8 @@ extension AppState { .eraseToAnyPublisher() } - public func installFromImage(dmgURL: URL) async throws { - - try? self.runtimeService.installRuntimeImage(dmgURL: dmgURL) - - } - - public func installFromPackage(dmgURL: URL, runtime: DownloadableRuntime) async throws { - Logger.appState.info("Mounting DMG") - - do { - let mountedUrl = try await self.runtimeService.mountDMG(dmgUrl: dmgURL) - - // 2-Get the first path under the mounted path, should be a .pkg - let pkgPath = Path(url: mountedUrl)!.ls().first! - try Path.xcodesCaches.mkdir().setCurrentUserAsOwner() - - let expandedPkgPath = Path.xcodesCaches/runtime.identifier - //try expandedPkgPath.mkdir() - Logger.appState.info("PKG Path: \(pkgPath)") - Logger.appState.info("Expanded PKG Path: \(expandedPkgPath)") - //try? Current.files.removeItem(at: expandedPkgPath.url) - - // 5-Expand (not install) the pkg to temporary path - try await self.runtimeService.expand(pkgPath: pkgPath, expandedPkgPath: expandedPkgPath) - //try await self.runtimeService.unmountDMG(mountedURL: mountedUrl) - - } catch { - Logger.appState.error("Error installing runtime: \(error.localizedDescription)") - } + try await self.runtimeService.installRuntimeImage(dmgURL: dmgURL) } } diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index d84ad26..3784891 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -240,6 +240,12 @@ public struct Files { return nil } } + + public var write: (Data, URL) throws -> Void = { try $0.write(to: $1) } + + public func write(_ data: Data, to url: URL) throws { + try write(data, url) + } } private func _installedXcodes(destination: Path) -> [InstalledXcode] { diff --git a/Xcodes/Frontend/InfoPane/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift index 625a5ae..399718d 100644 --- a/Xcodes/Frontend/InfoPane/InfoPane.swift +++ b/Xcodes/Frontend/InfoPane/InfoPane.swift @@ -288,8 +288,7 @@ struct InfoPane: View { switch runtime.installState { case .installing(let installationStep): - Text("INSTALLING") - InstallationStepDetailView(installationStep: installationStep) + RuntimeInstallationStepDetailView(installationStep: installationStep) .fixedSize(horizontal: false, vertical: true) default: EmptyView() diff --git a/Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift b/Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift new file mode 100644 index 0000000..f59f041 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift @@ -0,0 +1,53 @@ +// +// RuntimeInstallationStepDetailView.swift +// Xcodes +// +// Created by Matt Kiazyk on 2023-11-23. +// Copyright © 2023 Robots and Pencils. All rights reserved. +// + +import SwiftUI +import XcodesKit + +struct RuntimeInstallationStepDetailView: View { + let installationStep: RuntimeInstallationStep + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text(String(format: localizeString("InstallationStepDescription"), installationStep.stepNumber, installationStep.stepCount, installationStep.message)) + + switch installationStep { + case let .downloading(progress): + ObservingProgressIndicator( + progress, + controlSize: .regular, + style: .bar, + showsAdditionalDescription: true + ) + + case .installing, .trashingArchive: + ProgressView() + .scaleEffect(0.5) + } + } + } +} + +#Preview("Downloading") { + RuntimeInstallationStepDetailView( + installationStep: .downloading( + progress: configure(Progress()) { + $0.kind = .file + $0.fileOperationKind = .downloading + $0.estimatedTimeRemaining = 123 + $0.totalUnitCount = 11944848484 + $0.completedUnitCount = 848444920 + $0.throughput = 9211681 + } + )) +} +#Preview("Installing") { + RuntimeInstallationStepDetailView( + installationStep: .installing + ) +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift index 25bc5bc..84c4c8d 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift @@ -11,7 +11,7 @@ import Path public enum RuntimeInstallState: Equatable { case notInstalled case installing(RuntimeInstallationStep) - case installed(Path) + case installed var notInstalled: Bool { switch self { diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift index 21946a1..27b4e17 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift @@ -9,11 +9,8 @@ import Foundation public enum RuntimeInstallationStep: Equatable, CustomStringConvertible { case downloading(progress: Progress) - case unarchiving - case moving(destination: String) + case installing case trashingArchive - case checkingSecurity - case finishing public var description: String { "(\(stepNumber)/\(stepCount)) \(message)" @@ -23,29 +20,20 @@ public enum RuntimeInstallationStep: Equatable, CustomStringConvertible { switch self { case .downloading: return localizeString("Downloading") - case .unarchiving: - return localizeString("Unarchiving") - case .moving(let destination): - return String(format: localizeString("Moving"), destination) + case .installing: + return localizeString("Installing") case .trashingArchive: return localizeString("TrashingArchive") - case .checkingSecurity: - return localizeString("CheckingSecurity") - case .finishing: - return localizeString("Finishing") } } public var stepNumber: Int { switch self { case .downloading: return 1 - case .unarchiving: return 2 - case .moving: return 3 - case .trashingArchive: return 4 - case .checkingSecurity: return 5 - case .finishing: return 6 + case .installing: return 2 + case .trashingArchive: return 3 } } - public var stepCount: Int { 6 } + public var stepCount: Int { 3 } } diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift index c767207..5ffb33c 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift @@ -54,10 +54,8 @@ public struct RuntimeService { } } - public func installRuntimeImage(dmgURL: URL) throws { - Task { - _ = try await Current.shell.installRuntimeImage(dmgURL) - } + public func installRuntimeImage(dmgURL: URL) async throws { + _ = try await Current.shell.installRuntimeImage(dmgURL) } public func mountDMG(dmgUrl: URL) async throws -> URL { @@ -72,13 +70,20 @@ public struct RuntimeService { } public func unmountDMG(mountedURL: URL) async throws { - let url = try await Current.shell.unmountDmg(mountedURL) + _ = try await Current.shell.unmountDmg(mountedURL) } public func expand(pkgPath: Path, expandedPkgPath: Path) async throws { _ = try await Current.shell.expandPkg(pkgPath.url, expandedPkgPath.url) } - + + public func createPkg(pkgPath: Path, expandedPkgPath: Path) async throws { + _ = try await Current.shell.createPkg(pkgPath.url, expandedPkgPath.url) + } + + public func installPkg(pkgPath: Path, expandedPkgPath: Path) async throws { + _ = try await Current.shell.installPkg(pkgPath.url, expandedPkgPath.url.absoluteString) + } } extension String: Error {}