mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Merge pull request #39 from RobotsAndPencils/installation-step-view
Add InstallationStepView
This commit is contained in:
commit
2052ff54ff
7 changed files with 275 additions and 5 deletions
|
|
@ -12,6 +12,7 @@
|
|||
CA378F992466567600A58CE0 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA378F982466567600A58CE0 /* AppState.swift */; };
|
||||
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */; };
|
||||
CA44901F2463AD34003D8213 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA44901E2463AD34003D8213 /* Tag.swift */; };
|
||||
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */; };
|
||||
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */; };
|
||||
CA61A6E0259835580008926E /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA61A6DF259835580008926E /* Xcode.swift */; };
|
||||
CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA735108257BF96D00EA9CF8 /* AttributedText.swift */; };
|
||||
|
|
@ -61,6 +62,9 @@
|
|||
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFAA2B2592FBFC00380FEE /* Configure.swift */; };
|
||||
CABFAA432593104F00380FEE /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFAA422593104F00380FEE /* AboutView.swift */; };
|
||||
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */; };
|
||||
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 */; };
|
||||
CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD2E7A12449574E00113D76 /* XcodesApp.swift */; };
|
||||
CAD2E7A42449574E00113D76 /* XcodeListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD2E7A32449574E00113D76 /* XcodeListView.swift */; };
|
||||
CAD2E7A62449575000113D76 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAD2E7A52449575000113D76 /* Assets.xcassets */; };
|
||||
|
|
@ -123,6 +127,7 @@
|
|||
CA378F982466567600A58CE0 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
|
||||
CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreButtonStyle.swift; sourceTree = "<group>"; };
|
||||
CA44901E2463AD34003D8213 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = "<group>"; };
|
||||
CA538A0C255A4F1A00E64DD7 /* AppleAPI */ = {isa = PBXFileReference; lastKnownFileType = folder; name = AppleAPI; path = Xcodes/AppleAPI; sourceTree = "<group>"; };
|
||||
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCodeTextView.swift; sourceTree = "<group>"; };
|
||||
CA61A6DF259835580008926E /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -179,6 +184,9 @@
|
|||
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 = "<group>"; };
|
||||
CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+InfoPlistValues.swift"; sourceTree = "<group>"; };
|
||||
CAC281C7259F97E100B8AB0B /* InstallationStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepView.swift; sourceTree = "<group>"; };
|
||||
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservingProgressIndicator.swift; sourceTree = "<group>"; };
|
||||
CAC281D9259F985100B8AB0B /* InstallationStep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStep.swift; sourceTree = "<group>"; };
|
||||
CAD2E79E2449574E00113D76 /* Xcodes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Xcodes.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CAD2E7A12449574E00113D76 /* XcodesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodesApp.swift; sourceTree = "<group>"; };
|
||||
CAD2E7A32449574E00113D76 /* XcodeListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -236,7 +244,9 @@
|
|||
63EAA4E9259944340046AB8F /* Common */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */,
|
||||
63EAA4EA259944450046AB8F /* ProgressButton.swift */,
|
||||
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */,
|
||||
);
|
||||
path = Common;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -307,6 +317,7 @@
|
|||
CAE42486259A68A300B8B246 /* XcodeListCategory.swift */,
|
||||
CAD2E7A32449574E00113D76 /* XcodeListView.swift */,
|
||||
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */,
|
||||
CAC281C7259F97E100B8AB0B /* InstallationStepView.swift */,
|
||||
);
|
||||
path = XcodeList;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -329,6 +340,7 @@
|
|||
CABFA9AC2592EEE900380FEE /* Foundation.swift */,
|
||||
CA9FF8F425959CE000E47BAF /* HelperInstaller.swift */,
|
||||
CA9FF9352595B44700E47BAF /* HelperClient.swift */,
|
||||
CAC281D9259F985100B8AB0B /* InstallationStep.swift */,
|
||||
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
|
||||
CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */,
|
||||
CABFA9AE2592EEE900380FEE /* Path+.swift */,
|
||||
|
|
@ -624,6 +636,7 @@
|
|||
CABFA9BB2592EEEA00380FEE /* DateFormatter+.swift in Sources */,
|
||||
CABFA9BD2592EEEA00380FEE /* Environment.swift in Sources */,
|
||||
CABFA9C32592EEEA00380FEE /* Downloads.swift in Sources */,
|
||||
CAC281DA259F985100B8AB0B /* InstallationStep.swift in Sources */,
|
||||
CA378F992466567600A58CE0 /* AppState.swift in Sources */,
|
||||
CAD2E7A42449574E00113D76 /* XcodeListView.swift in Sources */,
|
||||
CAA1CB45255A5B60003FD669 /* SignIn2FAView.swift in Sources */,
|
||||
|
|
@ -632,6 +645,8 @@
|
|||
CA9FF8872595607900E47BAF /* InstalledXcode.swift in Sources */,
|
||||
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
|
||||
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
|
||||
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
|
||||
CAC281C8259F97E100B8AB0B /* InstallationStepView.swift in Sources */,
|
||||
CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */,
|
||||
CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */,
|
||||
CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */,
|
||||
|
|
@ -644,6 +659,7 @@
|
|||
CA9FF8F525959CE000E47BAF /* HelperInstaller.swift in Sources */,
|
||||
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
|
||||
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */,
|
||||
CAC281CD259F97FA00B8AB0B /* ObservingProgressIndicator.swift in Sources */,
|
||||
CAFBDC68259A308B003DCC5A /* InfoPane.swift in Sources */,
|
||||
CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */,
|
||||
CAFBDC6C259A3098003DCC5A /* View+Conditional.swift in Sources */,
|
||||
|
|
|
|||
45
Xcodes/Backend/InstallationStep.swift
Normal file
45
Xcodes/Backend/InstallationStep.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import Foundation
|
||||
|
||||
/// A numbered step
|
||||
enum InstallationStep: Equatable, CustomStringConvertible {
|
||||
case downloading(progress: Progress)
|
||||
case unarchiving
|
||||
case moving(destination: String)
|
||||
case trashingArchive
|
||||
case checkingSecurity
|
||||
case finishing
|
||||
|
||||
var description: String {
|
||||
"(\(stepNumber)/\(stepCount)) \(message)"
|
||||
}
|
||||
|
||||
var message: String {
|
||||
switch self {
|
||||
case .downloading:
|
||||
return "Downloading"
|
||||
case .unarchiving:
|
||||
return "Unarchiving (This can take a while)"
|
||||
case .moving(let destination):
|
||||
return "Moving to \(destination)"
|
||||
case .trashingArchive:
|
||||
return "Moving archive to the Trash"
|
||||
case .checkingSecurity:
|
||||
return "Security verification"
|
||||
case .finishing:
|
||||
return "Finishing"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
var stepCount: Int { 6 }
|
||||
}
|
||||
|
|
@ -47,6 +47,6 @@ struct Xcode: Identifiable, CustomStringConvertible {
|
|||
|
||||
enum XcodeInstallState: Equatable {
|
||||
case notInstalled
|
||||
case installing(Progress)
|
||||
case installing(InstallationStep)
|
||||
case installed
|
||||
}
|
||||
|
|
|
|||
62
Xcodes/Frontend/Common/ObservingProgressIndicator.swift
Normal file
62
Xcodes/Frontend/Common/ObservingProgressIndicator.swift
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
/// A ProgressIndicator that reflects the state of a Progress object.
|
||||
/// This functionality is already built in to ProgressView,
|
||||
/// but this implementation ensures that changes are received on the main thread.
|
||||
@available(iOS 14.0, macOS 11.0, *)
|
||||
public struct ObservingProgressIndicator: View {
|
||||
let controlSize: NSControl.ControlSize
|
||||
let style: NSProgressIndicator.Style
|
||||
@StateObject private var progress: ProgressWrapper
|
||||
|
||||
public init(
|
||||
_ progress: Progress,
|
||||
controlSize: NSControl.ControlSize,
|
||||
style: NSProgressIndicator.Style
|
||||
) {
|
||||
_progress = StateObject(wrappedValue: ProgressWrapper(progress: progress))
|
||||
self.controlSize = controlSize
|
||||
self.style = style
|
||||
}
|
||||
|
||||
class ProgressWrapper: ObservableObject {
|
||||
var progress: Progress
|
||||
var cancellable: AnyCancellable!
|
||||
|
||||
init(progress: Progress) {
|
||||
self.progress = progress
|
||||
cancellable = progress
|
||||
.publisher(for: \.fractionCompleted)
|
||||
.receive(on: RunLoop.main)
|
||||
.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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, *)
|
||||
struct ObservingProgressBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
ObservingProgressIndicator(
|
||||
configure(Progress(totalUnitCount: 100)) {
|
||||
$0.completedUnitCount = 40
|
||||
},
|
||||
controlSize: .small,
|
||||
style: .spinning
|
||||
)
|
||||
}
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
40
Xcodes/Frontend/Common/ProgressIndicator.swift
Normal file
40
Xcodes/Frontend/Common/ProgressIndicator.swift
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import SwiftUI
|
||||
import AppKit
|
||||
|
||||
/// You probably want ProgressView unless you need more of NSProgressIndicator's API, which this exposes.
|
||||
struct ProgressIndicator: NSViewRepresentable {
|
||||
typealias NSViewType = NSProgressIndicator
|
||||
|
||||
let minValue: Double
|
||||
let maxValue: Double
|
||||
let doubleValue: Double
|
||||
let controlSize: NSControl.ControlSize
|
||||
let isIndeterminate: Bool
|
||||
let style: NSProgressIndicator.Style
|
||||
|
||||
func makeNSView(context: Context) -> NSViewType {
|
||||
NSProgressIndicator()
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSViewType, context: Context) {
|
||||
nsView.minValue = minValue
|
||||
nsView.maxValue = maxValue
|
||||
nsView.doubleValue = doubleValue
|
||||
nsView.controlSize = controlSize
|
||||
nsView.isIndeterminate = isIndeterminate
|
||||
nsView.style = style
|
||||
}
|
||||
}
|
||||
|
||||
struct ProgressIndicator_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ProgressIndicator(
|
||||
minValue: 0,
|
||||
maxValue: 1,
|
||||
doubleValue: 0.4,
|
||||
controlSize: .small,
|
||||
isIndeterminate: false,
|
||||
style: .spinning
|
||||
)
|
||||
}
|
||||
}
|
||||
104
Xcodes/Frontend/XcodeList/InstallationStepView.swift
Normal file
104
Xcodes/Frontend/XcodeList/InstallationStepView.swift
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import SwiftUI
|
||||
|
||||
struct InstallationStepView: View {
|
||||
let installationStep: InstallationStep
|
||||
let highlighted: Bool
|
||||
let cancel: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
switch installationStep {
|
||||
case let .downloading(progress):
|
||||
// FB8955769 ProgressView.init(_: Progress) doesn't ensure that changes from the Progress object are applied to the UI on the main thread
|
||||
// This Progress is vended by URLSession so I don't think we can control that.
|
||||
// Use our own version of ProgressView that does this instead.
|
||||
ObservingProgressIndicator(
|
||||
progress,
|
||||
controlSize: .small,
|
||||
style: .spinning
|
||||
)
|
||||
case .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
|
||||
ProgressView()
|
||||
.scaleEffect(0.5)
|
||||
}
|
||||
|
||||
Text("Step \(installationStep.stepNumber) of \(installationStep.stepCount): \(installationStep.message)")
|
||||
.font(.footnote)
|
||||
|
||||
Button(action: { }) {
|
||||
Label("Cancel", systemImage: "xmark.circle.fill")
|
||||
.labelStyle(IconOnlyLabelStyle())
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.foregroundColor(highlighted ? .white : .secondary)
|
||||
.help("Cancel installation")
|
||||
}
|
||||
.frame(minWidth: 80)
|
||||
}
|
||||
}
|
||||
|
||||
struct InstallView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
ForEach(ColorScheme.allCases, id: \.self) { colorScheme in
|
||||
Group {
|
||||
InstallationStepView(
|
||||
installationStep: .downloading(
|
||||
progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 }
|
||||
),
|
||||
highlighted: false,
|
||||
cancel: {}
|
||||
)
|
||||
|
||||
InstallationStepView(
|
||||
installationStep: .unarchiving,
|
||||
highlighted: false,
|
||||
cancel: {}
|
||||
)
|
||||
|
||||
InstallationStepView(
|
||||
installationStep: .moving(destination: "/Applications"),
|
||||
highlighted: false,
|
||||
cancel: {}
|
||||
)
|
||||
|
||||
InstallationStepView(
|
||||
installationStep: .trashingArchive,
|
||||
highlighted: false,
|
||||
cancel: {}
|
||||
)
|
||||
|
||||
InstallationStepView(
|
||||
installationStep: .checkingSecurity,
|
||||
highlighted: false,
|
||||
cancel: {}
|
||||
)
|
||||
|
||||
InstallationStepView(
|
||||
installationStep: .finishing,
|
||||
highlighted: false,
|
||||
cancel: {}
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.windowBackgroundColor))
|
||||
.environment(\.colorScheme, colorScheme)
|
||||
}
|
||||
|
||||
ForEach(ColorScheme.allCases, id: \.self) { colorScheme in
|
||||
Group {
|
||||
InstallationStepView(
|
||||
installationStep: .downloading(
|
||||
progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 }
|
||||
),
|
||||
highlighted: true,
|
||||
cancel: {}
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.selectedContentBackgroundColor))
|
||||
.environment(\.colorScheme, colorScheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,14 +72,17 @@ struct XcodeListViewRow: View {
|
|||
|
||||
@ViewBuilder
|
||||
private func installControl(for xcode: Xcode) -> some View {
|
||||
if xcode.installed {
|
||||
switch xcode.installState {
|
||||
case .installed:
|
||||
Button("OPEN") { appState.open(id: xcode.id) }
|
||||
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: selected))
|
||||
.help("Open this version")
|
||||
} else {
|
||||
Button("INSTALL") { print("Installing...") }
|
||||
case .notInstalled:
|
||||
Button("INSTALL") { appState.install(id: xcode.id) }
|
||||
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: selected))
|
||||
.help("Install this version")
|
||||
case let .installing(installationStep):
|
||||
InstallationStepView(installationStep: installationStep, highlighted: selected, cancel: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -98,7 +101,7 @@ struct XcodeListViewRow_Previews: PreviewProvider {
|
|||
)
|
||||
|
||||
XcodeListViewRow(
|
||||
xcode: Xcode(version: Version("12.1.0")!, installState: .notInstalled, selected: false, path: nil, icon: nil),
|
||||
xcode: Xcode(version: Version("12.1.0")!, installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 })), selected: false, path: nil, icon: nil),
|
||||
selected: false
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue