diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 88c61ec..c7d1b70 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -75,7 +75,6 @@ CABFAA432593104F00380FEE /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFAA422593104F00380FEE /* AboutView.swift */; }; CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */; }; CAC28188259EE27200B8AB0B /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = CAC28187259EE27200B8AB0B /* CombineExpectations */; }; - CAC281C8259F97E100B8AB0B /* InstallationStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281C7259F97E100B8AB0B /* InstallationStepView.swift */; }; CAC281CD259F97FA00B8AB0B /* ObservingProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */; }; CAC281DA259F985100B8AB0B /* InstallationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281D9259F985100B8AB0B /* InstallationStep.swift */; }; CAC281E2259FA44600B8AB0B /* Bundle+XcodesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281E1259FA44600B8AB0B /* Bundle+XcodesTests.swift */; }; @@ -99,7 +98,10 @@ CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4AB325B7D3AF0064FE51 /* AdvancedPreferencePane.swift */; }; CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */; }; CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; }; + E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; }; E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; }; + E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; }; + E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -227,7 +229,6 @@ CABFAA2B2592FBFC00380FEE /* Configure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Configure.swift; path = Xcodes/Backend/Configure.swift; sourceTree = SOURCE_ROOT; }; CABFAA422593104F00380FEE /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+InfoPlistValues.swift"; sourceTree = ""; }; - CAC281C7259F97E100B8AB0B /* InstallationStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepView.swift; sourceTree = ""; }; CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservingProgressIndicator.swift; sourceTree = ""; }; CAC281D9259F985100B8AB0B /* InstallationStep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStep.swift; sourceTree = ""; }; CAC281E1259FA44600B8AB0B /* Bundle+XcodesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+XcodesTests.swift"; sourceTree = ""; }; @@ -247,7 +248,7 @@ CAE42486259A68A300B8B246 /* XcodeListCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListCategory.swift; sourceTree = ""; }; CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+IsNotNil.swift"; sourceTree = ""; }; CAE424B3259A764700B8B246 /* AppState+Install.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppState+Install.swift"; sourceTree = ""; }; - CAFBC3FF259AC17F00E2A3D8 /* InstallationStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepView.swift; sourceTree = ""; }; + CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepRowView.swift; sourceTree = ""; }; CAFBC421259ACF8000E2A3D8 /* ObservingProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservingProgressView.swift; sourceTree = ""; }; CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedXcode.swift; sourceTree = ""; }; CAFBDB942598FE96003DCC5A /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = ""; }; @@ -260,7 +261,9 @@ CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatesPreferencePane.swift; sourceTree = ""; }; CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListViewRow.swift; sourceTree = ""; }; CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = ""; }; + E87DD6EA25D053FA00D86808 /* Progress+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+.swift"; sourceTree = ""; }; E8977EA225C11E1500835F80 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; }; + E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -387,14 +390,12 @@ isa = PBXGroup; children = ( CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */, - CAFBDC67259A308B003DCC5A /* InfoPane.swift */, - CAFBC3FF259AC17F00E2A3D8 /* InstallationStepView.swift */, + CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */, CAFBDC4D2599B33D003DCC5A /* MainToolbar.swift */, CA44901E2463AD34003D8213 /* Tag.swift */, CAE42486259A68A300B8B246 /* XcodeListCategory.swift */, CAD2E7A32449574E00113D76 /* XcodeListView.swift */, CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */, - CAC281C7259F97E100B8AB0B /* InstallationStepView.swift */, ); path = XcodeList; sourceTree = ""; @@ -437,6 +438,7 @@ CA61A6DF259835580008926E /* Xcode.swift */, CA25192925A9644800F08414 /* XcodeInstallState.swift */, CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */, + E87DD6EA25D053FA00D86808 /* Progress+.swift */, ); path = Backend; sourceTree = ""; @@ -444,6 +446,7 @@ CABFAA1A2592F7D900380FEE /* Frontend */ = { isa = PBXGroup; children = ( + E8E98A9425D863B100EC89A0 /* InfoPane */, 63EAA4E9259944340046AB8F /* Common */, CA9FF8552595082000E47BAF /* About */, CAFE4AAA25B7D29B0064FE51 /* Preferences */, @@ -547,6 +550,15 @@ path = Preferences; sourceTree = ""; }; + E8E98A9425D863B100EC89A0 /* InfoPane */ = { + isa = PBXGroup; + children = ( + CAFBDC67259A308B003DCC5A /* InfoPane.swift */, + E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */, + ); + path = InfoPane; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -757,6 +769,7 @@ CABFA9BD2592EEEA00380FEE /* Environment.swift in Sources */, CABFA9C32592EEEA00380FEE /* Downloads.swift in Sources */, CAC281DA259F985100B8AB0B /* InstallationStep.swift in Sources */, + E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */, CA378F992466567600A58CE0 /* AppState.swift in Sources */, CAD2E7A42449574E00113D76 /* XcodeListView.swift in Sources */, CAA1CB45255A5B60003FD669 /* SignIn2FAView.swift in Sources */, @@ -768,7 +781,6 @@ CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */, CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */, CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */, - CAC281C8259F97E100B8AB0B /* InstallationStepView.swift in Sources */, CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */, CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */, CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */, @@ -782,6 +794,7 @@ CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */, CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */, CAC9F92D25BCDA4400B4965F /* HelperInstallState.swift in Sources */, + E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */, CAC281CD259F97FA00B8AB0B /* ObservingProgressIndicator.swift in Sources */, CABFA9C22592EEEA00380FEE /* Publisher+Resumable.swift in Sources */, CAFBDC68259A308B003DCC5A /* InfoPane.swift in Sources */, @@ -797,6 +810,7 @@ CA9FF87B2595293E00E47BAF /* DataSource.swift in Sources */, CABFA9C92592EEEA00380FEE /* URLRequest+Apple.swift in Sources */, CABFAA432593104F00380FEE /* AboutView.swift in Sources */, + E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */, CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */, CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */, 63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */, diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index dea687b..1093aa4 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -45,6 +45,7 @@ public struct Shell { "--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", "--dir=\(destination.parent.string)", "--out=\(destination.basename())", + "--human-readable=false", // sets the output to use bytes instead of formatting url.absoluteString, ] let stdOutPipe = Pipe() @@ -52,8 +53,10 @@ public struct Shell { let stdErrPipe = Pipe() process.standardError = stdErrPipe - var progress = Progress(totalUnitCount: 100) - + var progress = Progress() + progress.kind = .file + progress.fileOperationKind = .downloading + let observer = NotificationCenter.default.addObserver( forName: .NSFileHandleDataAvailable, object: nil, @@ -68,16 +71,8 @@ public struct Shell { defer { handle.waitForDataInBackgroundAndNotify() } let string = String(decoding: handle.availableData, as: UTF8.self) - let regex = try! NSRegularExpression(pattern: #"((?\d+)%\))"#) - let range = NSRange(location: 0, length: string.utf16.count) - - guard - let match = regex.firstMatch(in: string, options: [], range: range), - let matchRange = Range(match.range(withName: "percent"), in: string), - let percentCompleted = Int64(string[matchRange]) - else { return } - - progress.completedUnitCount = percentCompleted + + progress.updateFromAria2(string: string) } stdOutPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() diff --git a/Xcodes/Backend/Progress+.swift b/Xcodes/Backend/Progress+.swift new file mode 100644 index 0000000..b71b994 --- /dev/null +++ b/Xcodes/Backend/Progress+.swift @@ -0,0 +1,71 @@ +import os.log +import Foundation + +extension Progress { + func updateFromAria2(string: String) { + + let range = NSRange(location: 0, length: string.utf16.count) + + // MARK: Total Downloaded + let regexTotalDownloaded = try! NSRegularExpression(pattern: #"(?<= )(.*)(?=\/)"#) + + if let match = regexTotalDownloaded.firstMatch(in: string, options: [], range: range), + let matchRange = Range(match.range(at: 0), in: string), + let totalDownloaded = Int(string[matchRange].replacingOccurrences(of: "B", with: "")) { + self.completedUnitCount = Int64(totalDownloaded) + } + + // MARK: Filesize + let regexTotalFileSize = try! NSRegularExpression(pattern: #"(?<=/)(.*)(?=\()"#) + + if let match = regexTotalFileSize.firstMatch(in: string, options: [], range: range), + let matchRange = Range(match.range(at: 0), in: string), + let totalFileSize = Int(string[matchRange].replacingOccurrences(of: "B", with: "")) { + + if totalFileSize > 0 { + self.totalUnitCount = Int64(totalFileSize) + } + } + + // MARK: PERCENT DOWNLOADED + // Since we get fractionCompleted from completedUnitCount + totalUnitCount, no need to process + // let regexPercent = try! NSRegularExpression(pattern: #"((?\d+)%\))"#) + + // MARK: Speed + let regexSpeed = try! NSRegularExpression(pattern: #"(?<=DL:)(.*)(?= )"#) + + if let match = regexSpeed.firstMatch(in: string, options: [], range: range), + let matchRange = Range(match.range(at: 0), in: string), + let speed = Int(string[matchRange].replacingOccurrences(of: "B", with: "")) { + self.throughput = speed + } else { + Logger.appState.debug("Could not parse throughput from aria2 download output") + } + + // MARK: Estimated Time Remaining + let regexETA = try! NSRegularExpression(pattern: #"(?<=ETA:)(?\d*h)?(?\d*m)?(?\d*s)?"#) + + if let match = regexETA.firstMatch(in: string, options: [], range: range) { + var seconds: Int = 0 + + if let matchRange = Range(match.range(withName: "hours"), in: string), + let hours = Int(string[matchRange].replacingOccurrences(of: "h", with: "")) { + seconds += (hours * 60 * 60) + } + + if let matchRange = Range(match.range(withName: "minutes"), in: string), + let minutes = Int(string[matchRange].replacingOccurrences(of: "m", with: "")) { + seconds += (minutes * 60) + } + + if let matchRange = Range(match.range(withName: "seconds"), in: string), + let second = Int(string[matchRange].replacingOccurrences(of: "s", with: "")) { + seconds += (second) + } + + self.estimatedTimeRemaining = TimeInterval(seconds) + } + + } +} + diff --git a/Xcodes/Frontend/Common/ObservingProgressIndicator.swift b/Xcodes/Frontend/Common/ObservingProgressIndicator.swift index 88f0b05..d4dc428 100644 --- a/Xcodes/Frontend/Common/ObservingProgressIndicator.swift +++ b/Xcodes/Frontend/Common/ObservingProgressIndicator.swift @@ -8,16 +8,19 @@ import SwiftUI public struct ObservingProgressIndicator: View { let controlSize: NSControl.ControlSize let style: NSProgressIndicator.Style + let showsAdditionalDescription: Bool @StateObject private var progress: ProgressWrapper public init( _ progress: Progress, controlSize: NSControl.ControlSize, - style: NSProgressIndicator.Style + style: NSProgressIndicator.Style, + showsAdditionalDescription: Bool = false ) { _progress = StateObject(wrappedValue: ProgressWrapper(progress: progress)) self.controlSize = controlSize self.style = style + self.showsAdditionalDescription = showsAdditionalDescription } class ProgressWrapper: ObservableObject { @@ -26,23 +29,31 @@ public struct ObservingProgressIndicator: View { init(progress: Progress) { self.progress = progress - cancellable = progress - .publisher(for: \.fractionCompleted) - .receive(on: RunLoop.main) + cancellable = progress.publisher(for: \.fractionCompleted) + .combineLatest(progress.publisher(for: \.localizedAdditionalDescription)) + .throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true) .sink { [weak self] _ in self?.objectWillChange.send() } } } public var body: some View { - ProgressIndicator( - minValue: 0.0, - maxValue: 1.0, - doubleValue: progress.progress.fractionCompleted, - controlSize: controlSize, - isIndeterminate: progress.progress.isIndeterminate, - style: style - ) - .help("Downloading: \(Int((progress.progress.fractionCompleted * 100)))% complete") + VStack(alignment: .leading, spacing: 0) { + ProgressIndicator( + minValue: 0.0, + maxValue: 1.0, + doubleValue: progress.progress.fractionCompleted, + controlSize: controlSize, + isIndeterminate: progress.progress.isIndeterminate, + style: style + ) + .help("Downloading: \(Int((progress.progress.fractionCompleted * 100)))% complete") + + if showsAdditionalDescription, progress.progress.localizedAdditionalDescription.isEmpty == false { + Text(progress.progress.localizedAdditionalDescription) + .font(.subheadline) + .foregroundColor(.secondary) + } + } } } @@ -57,6 +68,20 @@ struct ObservingProgressBar_Previews: PreviewProvider { controlSize: .small, style: .spinning ) + + ObservingProgressIndicator( + configure(Progress()) { + $0.kind = .file + $0.fileOperationKind = .downloading + $0.estimatedTimeRemaining = 123 + $0.totalUnitCount = 11944848484 + $0.completedUnitCount = 848444920 + $0.throughput = 9211681 + }, + controlSize: .regular, + style: .bar, + showsAdditionalDescription: true + ) } .previewLayout(.sizeThatFits) } diff --git a/Xcodes/Frontend/XcodeList/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift similarity index 81% rename from Xcodes/Frontend/XcodeList/InfoPane.swift rename to Xcodes/Frontend/InfoPane/InfoPane.swift index 53d5088..93b7627 100644 --- a/Xcodes/Frontend/XcodeList/InfoPane.swift +++ b/Xcodes/Frontend/InfoPane/InfoPane.swift @@ -13,51 +13,53 @@ struct InfoPane: View { var body: some View { if let xcode = appState.allXcodes.first(where: { $0.id == selectedXcodeID }) { ScrollView { - VStack(spacing: 16) { + VStack(alignment: .leading, spacing: 16) { icon(for: xcode) + .frame(maxWidth: .infinity, alignment: .center) + + Text(verbatim: "Xcode \(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)") + .font(.title) - VStack(alignment: .leading) { - Text(verbatim: "Xcode \(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)") - .font(.title) - .frame(maxWidth: .infinity, alignment: .leading) + switch xcode.installState { + case .notInstalled: + InstallButton(xcode: xcode) + downloadFileSize(for: xcode) + case .installing(let installationStep): + InstallationStepDetailView(installationStep: installationStep) + .fixedSize(horizontal: false, vertical: true) + CancelInstallButton(xcode: xcode) + case let .installed(path): + HStack { + Text(path.string) + Button(action: { appState.reveal(id: xcode.id) }) { + Image(systemName: "arrow.right.circle.fill") + } + .buttonStyle(PlainButtonStyle()) + .help("Reveal in Finder") + } - switch xcode.installState { - case .notInstalled: - InstallButton(xcode: xcode) - case .installing: - CancelInstallButton(xcode: xcode) - case let .installed(path): - HStack { - Text(path.string) - Button(action: { appState.reveal(id: xcode.id) }) { - Image(systemName: "arrow.right.circle.fill") - } - .buttonStyle(PlainButtonStyle()) - .help("Reveal in Finder") - } + HStack { + SelectButton(xcode: xcode) + .disabled(xcode.selected) + .help("Selected") - HStack { - SelectButton(xcode: xcode) - .disabled(xcode.selected) - .help("Selected") - - OpenButton(xcode: xcode) - .help("Open") - - Spacer() - UninstallButton(xcode: xcode) - } + OpenButton(xcode: xcode) + .help("Open") + + Spacer() + UninstallButton(xcode: xcode) } } Divider() - - releaseNotes(for: xcode) - identicalBuilds(for: xcode) - compatibility(for: xcode) - sdks(for: xcode) - compilers(for: xcode) - downloadFileSize(for: xcode) + + Group{ + releaseNotes(for: xcode) + identicalBuilds(for: xcode) + compatibility(for: xcode) + sdks(for: xcode) + compilers(for: xcode) + } Spacer() } @@ -210,7 +212,6 @@ struct InfoPane: View { } } - @ViewBuilder private var empty: some View { Text("No Xcode Selected") @@ -252,7 +253,7 @@ struct InfoPane_Previews: PreviewProvider { ] }) .previewDisplayName("Populated, Installed, Selected") - + InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0)) .environmentObject(configure(AppState()) { $0.allXcodes = [ @@ -278,7 +279,7 @@ struct InfoPane_Previews: PreviewProvider { ] }) .previewDisplayName("Populated, Installed, Unselected") - + InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0)) .environmentObject(configure(AppState()) { $0.allXcodes = [ @@ -304,7 +305,7 @@ struct InfoPane_Previews: PreviewProvider { ] }) .previewDisplayName("Populated, Uninstalled") - + InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 1, buildMetadataIdentifiers: ["1234A"])) .environmentObject(configure(AppState()) { $0.allXcodes = [ @@ -318,6 +319,20 @@ struct InfoPane_Previews: PreviewProvider { ] }) .previewDisplayName("Basic, installed") + + InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 1, buildMetadataIdentifiers: ["1234A"])) + .environmentObject(configure(AppState()) { + $0.allXcodes = [ + .init( + version: Version(major: 12, minor: 3, patch: 1, buildMetadataIdentifiers: ["1234A"]), + installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40; $0.throughput = 232323232; $0.fileCompletedCount = 2323004; $0.fileTotalCount = 1193939393 })), + selected: false, + icon: nil, + sdks: nil, + compilers: nil) + ] + }) + .previewDisplayName("Basic, installing") InfoPane(selectedXcodeID: nil) .environmentObject(configure(AppState()) { diff --git a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift new file mode 100644 index 0000000..a1992d5 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift @@ -0,0 +1,48 @@ +import SwiftUI + +struct InstallationStepDetailView: View { + let installationStep: InstallationStep + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text("Step \(installationStep.stepNumber) of \(installationStep.stepCount): \(installationStep.message)") + + switch installationStep { + case let .downloading(progress): + ObservingProgressIndicator( + progress, + controlSize: .regular, + style: .bar, + showsAdditionalDescription: true + ) + + case .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing: + ProgressView() + .scaleEffect(0.5) + } + } + } +} + +struct InstallDetailView_Previews: PreviewProvider { + static var previews: some View { + Group { + InstallationStepDetailView( + installationStep: .downloading( + progress: configure(Progress()) { + $0.kind = .file + $0.fileOperationKind = .downloading + $0.estimatedTimeRemaining = 123 + $0.totalUnitCount = 11944848484 + $0.completedUnitCount = 848444920 + $0.throughput = 9211681 + } + ) + ) + + InstallationStepDetailView( + installationStep: .unarchiving + ) + } + } +} diff --git a/Xcodes/Frontend/XcodeList/InstallationStepView.swift b/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift similarity index 91% rename from Xcodes/Frontend/XcodeList/InstallationStepView.swift rename to Xcodes/Frontend/XcodeList/InstallationStepRowView.swift index 42d3f25..92d3c2d 100644 --- a/Xcodes/Frontend/XcodeList/InstallationStepView.swift +++ b/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift @@ -1,6 +1,6 @@ import SwiftUI -struct InstallationStepView: View { +struct InstallationStepRowView: View { let installationStep: InstallationStep let highlighted: Bool let cancel: () -> Void @@ -42,7 +42,7 @@ struct InstallView_Previews: PreviewProvider { Group { ForEach(ColorScheme.allCases, id: \.self) { colorScheme in Group { - InstallationStepView( + InstallationStepRowView( installationStep: .downloading( progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 } ), @@ -50,31 +50,31 @@ struct InstallView_Previews: PreviewProvider { cancel: {} ) - InstallationStepView( + InstallationStepRowView( installationStep: .unarchiving, highlighted: false, cancel: {} ) - InstallationStepView( + InstallationStepRowView( installationStep: .moving(destination: "/Applications"), highlighted: false, cancel: {} ) - InstallationStepView( + InstallationStepRowView( installationStep: .trashingArchive, highlighted: false, cancel: {} ) - InstallationStepView( + InstallationStepRowView( installationStep: .checkingSecurity, highlighted: false, cancel: {} ) - InstallationStepView( + InstallationStepRowView( installationStep: .finishing, highlighted: false, cancel: {} @@ -87,7 +87,7 @@ struct InstallView_Previews: PreviewProvider { ForEach(ColorScheme.allCases, id: \.self) { colorScheme in Group { - InstallationStepView( + InstallationStepRowView( installationStep: .downloading( progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 } ), diff --git a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift index 723d690..9b0c645 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift @@ -109,7 +109,7 @@ struct XcodeListViewRow: View { .buttonStyle(AppStoreButtonStyle(primary: false, highlighted: selected)) .help("Install this version") case let .installing(installationStep): - InstallationStepView( + InstallationStepRowView( installationStep: installationStep, highlighted: selected, cancel: { appState.xcodeBeingConfirmedForInstallCancellation = xcode }