Merge pull request #27 from RobotsAndPencils/inspector

Add inspector pane
This commit is contained in:
Brandon Evans 2020-12-28 14:50:55 -07:00 committed by GitHub
commit e3f07e855e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 497 additions and 87 deletions

View file

@ -69,9 +69,14 @@
CAD2E7A62449575000113D76 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAD2E7A52449575000113D76 /* Assets.xcassets */; };
CAD2E7A92449575000113D76 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAD2E7A82449575000113D76 /* Preview Assets.xcassets */; };
CAD2E7B82449575100113D76 /* XcodesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD2E7B72449575100113D76 /* XcodesTests.swift */; };
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE4247E259A666100B8B246 /* MainWindow.swift */; };
CAE42487259A68A300B8B246 /* XcodeListCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE42486259A68A300B8B246 /* XcodeListCategory.swift */; };
CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */; };
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */; };
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDB942598FE96003DCC5A /* FocusedValues.swift */; };
CAFBDC4E2599B33D003DCC5A /* MainToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDC4D2599B33D003DCC5A /* MainToolbar.swift */; };
CAFBDC68259A308B003DCC5A /* InspectorPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDC67259A308B003DCC5A /* InspectorPane.swift */; };
CAFBDC6C259A3098003DCC5A /* View+Conditional.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDC6B259A3098003DCC5A /* View+Conditional.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -187,10 +192,15 @@
CAD2E7B32449575100113D76 /* XcodesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XcodesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
CAD2E7B72449575100113D76 /* XcodesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodesTests.swift; sourceTree = "<group>"; };
CAD2E7B92449575100113D76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CAE4247E259A666100B8B246 /* MainWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindow.swift; sourceTree = "<group>"; };
CAE42486259A68A300B8B246 /* XcodeListCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListCategory.swift; sourceTree = "<group>"; };
CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+IsNotNil.swift"; sourceTree = "<group>"; };
CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedXcode.swift; sourceTree = "<group>"; };
CAFBDB942598FE96003DCC5A /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = "<group>"; };
CAFBDBA525990C76003DCC5A /* SimpleXPCApp.LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = SimpleXPCApp.LICENSE; sourceTree = "<group>"; };
CAFBDC4D2599B33D003DCC5A /* MainToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainToolbar.swift; sourceTree = "<group>"; };
CAFBDC67259A308B003DCC5A /* InspectorPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorPane.swift; sourceTree = "<group>"; };
CAFBDC6B259A3098003DCC5A /* View+Conditional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Conditional.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -295,8 +305,10 @@
isa = PBXGroup;
children = (
CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */,
CAFBDC67259A308B003DCC5A /* InspectorPane.swift */,
CAFBDC4D2599B33D003DCC5A /* MainToolbar.swift */,
CA44901E2463AD34003D8213 /* Tag.swift */,
CAE42486259A68A300B8B246 /* XcodeListCategory.swift */,
CAD2E7A32449574E00113D76 /* XcodeListView.swift */,
);
path = XcodeList;
@ -321,6 +333,7 @@
CA9FF8F425959CE000E47BAF /* HelperInstaller.swift */,
CA9FF9352595B44700E47BAF /* HelperClient.swift */,
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */,
CABFA9AE2592EEE900380FEE /* Path+.swift */,
CABFA9B42592EEEA00380FEE /* Process.swift */,
CABFA9B02592EEEA00380FEE /* Promise+.swift */,
@ -343,7 +356,9 @@
CA9FF8552595082000E47BAF /* About */,
CAA1CB50255A5D16003FD669 /* SignIn */,
CABFAA142592F73000380FEE /* XcodeList */,
CAE4247E259A666100B8B246 /* MainWindow.swift */,
CABFAA2A2592FBFC00380FEE /* SettingsView.swift */,
CAFBDC6B259A3098003DCC5A /* View+Conditional.swift */,
CA9FF8652595130600E47BAF /* View+IsHidden.swift */,
);
path = Frontend;
@ -576,6 +591,7 @@
outputFileListPaths = (
);
outputPaths = (
"$(SRCROOT)/Xcodes/Resources/Licenses.rtf",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@ -606,6 +622,7 @@
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */,
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */,
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
CABFA9CA2592EEEA00380FEE /* AppState+Update.swift in Sources */,
CA44901F2463AD34003D8213 /* Tag.swift in Sources */,
@ -620,6 +637,7 @@
CABFA9CD2592EEEA00380FEE /* Foundation.swift in Sources */,
CA9FF8872595607900E47BAF /* InstalledXcode.swift in Sources */,
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */,
CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */,
CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */,
@ -633,9 +651,12 @@
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */,
CABFA9C22592EEEA00380FEE /* Promise+.swift in Sources */,
CAFBDC68259A308B003DCC5A /* InspectorPane.swift in Sources */,
CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */,
CAFBDC6C259A3098003DCC5A /* View+Conditional.swift in Sources */,
CABFA9CF2592EEEA00380FEE /* Process.swift in Sources */,
CABFA9C72592EEEA00380FEE /* Entry+.swift in Sources */,
CAE42487259A68A300B8B246 /* XcodeListCategory.swift in Sources */,
CABFAA2C2592FBFC00380FEE /* SettingsView.swift in Sources */,
CA9FF87B2595293E00E47BAF /* DataSource.swift in Sources */,
CABFA9C92592EEEA00380FEE /* URLRequest+Apple.swift in Sources */,

View file

@ -170,7 +170,11 @@ extension AppState {
version: version,
url: downloadURL,
filename: String(downloadURL.path.suffix(fromLast: "/")),
releaseDate: releaseDate
releaseDate: releaseDate,
requiredMacOSVersion: xcReleasesXcode.requires,
releaseNotesURL: xcReleasesXcode.links?.notes?.url,
sdks: xcReleasesXcode.sdks,
compilers: xcReleasesXcode.compilers
)
}
return xcodes

View file

@ -237,7 +237,7 @@ class AppState: ObservableObject {
)
}
func launch(id: Xcode.ID) {
func open(id: Xcode.ID) {
guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }) else { return }
NSWorkspace.shared.openApplication(at: installedXcode.path.url, configuration: .init())
}
@ -274,12 +274,17 @@ class AppState: ObservableObject {
.sorted(by: >)
.map { xcodeVersion in
let installedXcode = installedXcodes.first(where: { xcodeVersion.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) })
let availableXcode = xcodes.first { $0.version == xcodeVersion }
return Xcode(
version: xcodeVersion,
installState: installedXcodes.contains(where: { xcodeVersion.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) }) ? .installed : .notInstalled,
selected: false,
path: installedXcode?.path.string,
icon: (installedXcode?.path.string).map(NSWorkspace.shared.icon(forFile:))
icon: (installedXcode?.path.string).map(NSWorkspace.shared.icon(forFile:)),
requiredMacOSVersion: availableXcode?.requiredMacOSVersion,
releaseNotesURL: availableXcode?.releaseNotesURL,
sdks: availableXcode?.sdks,
compilers: availableXcode?.compilers
)
}
}

View file

@ -1,5 +1,7 @@
import Foundation
import Version
import struct XCModel.SDKs
import struct XCModel.Compilers
/// A version of Xcode that's available for installation
public struct AvailableXcode: Codable {
@ -7,11 +9,28 @@ public struct AvailableXcode: Codable {
public let url: URL
public let filename: String
public let releaseDate: Date?
public let requiredMacOSVersion: String?
public let releaseNotesURL: URL?
public let sdks: SDKs?
public let compilers: Compilers?
public init(version: Version, url: URL, filename: String, releaseDate: Date?) {
public init(
version: Version,
url: URL,
filename: String,
releaseDate: Date?,
requiredMacOSVersion: String? = nil,
releaseNotesURL: URL? = nil,
sdks: SDKs? = nil,
compilers: Compilers? = nil
) {
self.version = version
self.url = url
self.filename = filename
self.releaseDate = releaseDate
self.requiredMacOSVersion = requiredMacOSVersion
self.releaseNotesURL = releaseNotesURL
self.sdks = sdks
self.compilers = compilers
}
}

View file

@ -0,0 +1,9 @@
import Foundation
extension Optional {
/// Note that this is lossy when setting, so you can really only set it to nil, but this is sufficient for mapping `Binding<Item?>` to `Binding<Bool>` for Alerts, Popovers, etc.
var isNotNil: Bool {
get { self != nil }
set { self = newValue ? self : nil }
}
}

View file

@ -1,6 +1,8 @@
import AppKit
import Foundation
import Version
import struct XCModel.SDKs
import struct XCModel.Compilers
struct Xcode: Identifiable, CustomStringConvertible {
let version: Version
@ -8,6 +10,32 @@ struct Xcode: Identifiable, CustomStringConvertible {
let selected: Bool
let path: String?
let icon: NSImage?
let requiredMacOSVersion: String?
let releaseNotesURL: URL?
let sdks: SDKs?
let compilers: Compilers?
init(
version: Version,
installState: XcodeInstallState,
selected: Bool,
path: String?,
icon: NSImage?,
requiredMacOSVersion: String? = nil,
releaseNotesURL: URL? = nil,
sdks: SDKs? = nil,
compilers: Compilers? = nil
) {
self.version = version
self.installState = installState
self.selected = selected
self.path = path
self.icon = icon
self.requiredMacOSVersion = requiredMacOSVersion
self.releaseNotesURL = releaseNotesURL
self.sdks = sdks
self.compilers = compilers
}
var id: Version { version }
var installed: Bool { installState == .installed }

View file

@ -14,7 +14,7 @@ struct XcodeCommands: Commands {
Divider()
SelectCommand()
LaunchCommand()
OpenCommand()
RevealCommand()
CopyPathCommand()
}
@ -66,19 +66,19 @@ struct SelectButton: View {
}
}
struct LaunchButton: View {
struct OpenButton: View {
@EnvironmentObject var appState: AppState
let xcode: Xcode?
var body: some View {
Button(action: launch) {
Text("Launch")
Button(action: open) {
Text("Open")
}
}
private func launch() {
private func open() {
guard let xcode = xcode else { return }
appState.launch(id: xcode.id)
appState.open(id: xcode.id)
}
}
@ -138,12 +138,12 @@ struct SelectCommand: View {
}
}
struct LaunchCommand: View {
struct OpenCommand: View {
@EnvironmentObject var appState: AppState
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
var body: some View {
LaunchButton(xcode: selectedXcode.unwrapped)
OpenButton(xcode: selectedXcode.unwrapped)
.keyboardShortcut(KeyboardShortcut(.downArrow, modifiers: .command))
.disabled(selectedXcode.unwrapped?.installed != true)
}

View file

@ -0,0 +1,74 @@
import SwiftUI
struct MainWindow: View {
@EnvironmentObject var appState: AppState
@State private var selection: Xcode.ID?
@State private var searchText: String = ""
@AppStorage("lastUpdated") private var lastUpdated: Double?
@SceneStorage("isShowingInfoPane") private var isShowingInfoPane = false
@SceneStorage("xcodeListCategory") private var category: XcodeListCategory = .all
var body: some View {
HSplitView {
XcodeListView(searchText: searchText, category: category)
.frame(minWidth: 300)
.layoutPriority(1)
InspectorPane()
.frame(minWidth: 300, maxWidth: .infinity)
.frame(width: isShowingInfoPane ? nil : 0)
.isHidden(!isShowingInfoPane)
}
.mainToolbar(
category: $category,
isShowingInfoPane: $isShowingInfoPane,
searchText: $searchText
)
.navigationSubtitle(subtitleText)
.frame(minWidth: 600, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
.alert(item: $appState.error) { error in
Alert(title: Text(error.title),
message: Text(verbatim: error.message),
dismissButton: .default(Text("OK")))
}
/*
Removing this for now, because it's overriding the error alert that's being worked on above.
.alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in
Alert(title: Text("Uninstall Xcode \(xcode.description)?"),
message: Text("It will be moved to the Trash, but won't be emptied."),
primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: xcode.id) }),
secondaryButton: .cancel(Text("Cancel")))
}
**/
.sheet(isPresented: $appState.secondFactorData.isNotNil) {
secondFactorView(appState.secondFactorData!)
.environmentObject(appState)
}
}
private var subtitleText: Text {
if let lastUpdated = lastUpdated.map(Date.init(timeIntervalSince1970:)) {
return Text("Updated at \(lastUpdated, style: .date) \(lastUpdated, style: .time)")
} else {
return Text("")
}
}
@ViewBuilder
private func secondFactorView(_ secondFactorData: AppState.SecondFactorData) -> some View {
switch secondFactorData.option {
case .codeSent:
SignIn2FAView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
case .smsSent(let trustedPhoneNumber):
SignInSMSView(isPresented: $appState.secondFactorData.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
case .smsPendingChoice:
SignInPhoneListView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
}
}
}
struct MainWindow_Previews: PreviewProvider {
static var previews: some View {
MainWindow()
}
}

View file

@ -0,0 +1,12 @@
import SwiftUI
extension View {
@ViewBuilder
func `if`<Other: View>(_ predicate: Bool, then: (Self) -> Other) -> some View {
if predicate {
then(self)
} else {
self
}
}
}

View file

@ -0,0 +1,265 @@
import AppKit
import SwiftUI
import Version
import struct XCModel.SDKs
import struct XCModel.Compilers
struct InspectorPane: View {
@EnvironmentObject var appState: AppState
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
var body: some View {
Group {
if let xcode = appState.allXcodes.first(where: { $0.id == appState.selectedXcodeID }) {
VStack(spacing: 16) {
icon(for: xcode)
VStack(alignment: .leading) {
Text("Xcode \(xcode.description)")
.font(.title)
.frame(maxWidth: .infinity, alignment: .leading)
if let path = xcode.path {
HStack {
Text(path)
Button(action: { appState.reveal(id: xcode.id) }) {
Image(systemName: "arrow.right.circle.fill")
}
.buttonStyle(PlainButtonStyle())
}
HStack {
if xcode.selected {
Button("Selected", action: {})
.disabled(true)
} else {
SelectButton(xcode: xcode)
}
OpenButton(xcode: xcode)
}
} else {
InstallButton(xcode: xcode)
}
}
Divider()
releaseNotes(for: xcode)
compatibility(for: xcode)
sdks(for: xcode)
compilers(for: xcode)
Spacer()
}
} else {
empty
}
}
.padding()
.frame(minWidth: 200, maxWidth: .infinity)
}
@ViewBuilder
private func icon(for xcode: Xcode) -> some View {
if let path = xcode.path {
Image(nsImage: NSWorkspace.shared.icon(forFile: path))
} else {
Image(systemName: "app.fill")
.resizable()
.frame(width: 32, height: 32)
.foregroundColor(.secondary)
}
}
@ViewBuilder
private func releaseNotes(for xcode: Xcode) -> some View {
if let releaseNotesURL = xcode.releaseNotesURL {
Button(action: { openURL(releaseNotesURL) }) {
Label("Release Notes", systemImage: "link")
}
.buttonStyle(LinkButtonStyle())
.frame(maxWidth: .infinity, alignment: .leading)
} else {
EmptyView()
}
}
@ViewBuilder
private func compatibility(for xcode: Xcode) -> some View {
if let requiredMacOSVersion = xcode.requiredMacOSVersion {
VStack(alignment: .leading) {
Text("Compatibility")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
Text("Requires macOS \(requiredMacOSVersion) or later")
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .leading)
}
} else {
EmptyView()
}
}
@ViewBuilder
private func sdks(for xcode: Xcode) -> some View {
if let sdks = xcode.sdks {
VStack(alignment: .leading) {
Text("SDKs")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
ForEach([
("macOS", \SDKs.macOS),
("iOS", \.iOS),
("watchOS", \.watchOS),
("tvOS", \.tvOS),
], id: \.0) { row in
if let sdk = sdks[keyPath: row.1] {
Text("\(row.0): \(sdk.compactMap { $0.number }.joined(separator: ", "))")
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
} else {
EmptyView()
}
}
@ViewBuilder
private func compilers(for xcode: Xcode) -> some View {
if let compilers = xcode.compilers {
VStack(alignment: .leading) {
Text("Compilers")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
ForEach([
("Swift", \Compilers.swift),
("Clang", \.clang),
("LLVM", \.llvm),
("LLVM GCC", \.llvm_gcc),
("GCC", \.gcc),
], id: \.0) { row in
if let sdk = compilers[keyPath: row.1] {
Text("\(row.0): \(sdk.compactMap { $0.number }.joined(separator: ", "))")
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
} else {
EmptyView()
}
}
@ViewBuilder
private var empty: some View {
VStack {
Spacer()
Text("No Xcode Selected")
.font(.title)
.foregroundColor(.secondary)
Spacer()
}
}
}
struct InspectorPane_Previews: PreviewProvider {
static var previews: some View {
Group {
InspectorPane()
.environmentObject(configure(AppState()) {
$0.allXcodes = [
.init(
version: Version(major: 12, minor: 3, patch: 0),
installState: .installed,
selected: true,
path: "/Applications/Xcode-12.3.0.app",
icon: NSWorkspace.shared.icon(forFile: "/Applications/Xcode-12.3.0.app"),
requiredMacOSVersion: "10.15.4",
releaseNotesURL: URL(string: "https://developer.apple.com/documentation/xcode-release-notes/xcode-12_3-release-notes/")!,
sdks: SDKs(
macOS: .init(number: "11.1"),
iOS: .init(number: "14.3"),
watchOS: .init(number: "7.3"),
tvOS: .init(number: "14.3")
),
compilers: Compilers(
gcc: .init(number: "4"),
llvm_gcc: .init(number: "213"),
llvm: .init(number: "2.3"),
clang: .init(number: "7.3"),
swift: .init(number: "5.3.2")
))
]
$0.selectedXcodeID = Version(major: 12, minor: 3, patch: 0)
})
.previewDisplayName("Populated, Installed, Selected")
InspectorPane()
.environmentObject(configure(AppState()) {
$0.allXcodes = [
.init(
version: Version(major: 12, minor: 3, patch: 0),
installState: .installed,
selected: false,
path: "/Applications/Xcode-12.3.0.app",
icon: NSWorkspace.shared.icon(forFile: "/Applications/Xcode-12.3.0.app"),
sdks: SDKs(
macOS: .init(number: "11.1"),
iOS: .init(number: "14.3"),
watchOS: .init(number: "7.3"),
tvOS: .init(number: "14.3")
),
compilers: Compilers(
gcc: .init(number: "4"),
llvm_gcc: .init(number: "213"),
llvm: .init(number: "2.3"),
clang: .init(number: "7.3"),
swift: .init(number: "5.3.2")
))
]
$0.selectedXcodeID = Version(major: 12, minor: 3, patch: 0)
})
.previewDisplayName("Populated, Installed, Unselected")
InspectorPane()
.environmentObject(configure(AppState()) {
$0.allXcodes = [
.init(
version: Version(major: 12, minor: 3, patch: 0),
installState: .notInstalled,
selected: false,
path: nil,
icon: nil,
sdks: SDKs(
macOS: .init(number: "11.1"),
iOS: .init(number: "14.3"),
watchOS: .init(number: "7.3"),
tvOS: .init(number: "14.3")
),
compilers: Compilers(
gcc: .init(number: "4"),
llvm_gcc: .init(number: "213"),
llvm: .init(number: "2.3"),
clang: .init(number: "7.3"),
swift: .init(number: "5.3.2")
))
]
$0.selectedXcodeID = Version(major: 12, minor: 3, patch: 0)
})
.previewDisplayName("Populated, Uninstalled")
InspectorPane()
.environmentObject(configure(AppState()) {
$0.allXcodes = [
]
$0.selectedXcodeID = nil
})
.previewDisplayName("Empty")
}
.frame(maxWidth: 300)
}
}

View file

@ -2,7 +2,8 @@ import SwiftUI
struct MainToolbarModifier: ViewModifier {
@EnvironmentObject var appState: AppState
@Binding var category: XcodeListView.Category
@Binding var category: XcodeListCategory
@Binding var isShowingInfoPane: Bool
@Binding var searchText: String
func body(content: Content) -> some View {
@ -35,6 +36,16 @@ struct MainToolbarModifier: ViewModifier {
}
}
Button(action: { isShowingInfoPane.toggle() }) {
if isShowingInfoPane {
Label("Inspector", systemImage: "info.circle.fill")
.foregroundColor(.accentColor)
} else {
Label("Inspector", systemImage: "info.circle")
}
}
.keyboardShortcut(KeyboardShortcut("i", modifiers: [.command, .option]))
TextField("Search...", text: $searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 200)
@ -44,9 +55,16 @@ struct MainToolbarModifier: ViewModifier {
extension View {
func mainToolbar(
category: Binding<XcodeListView.Category>,
category: Binding<XcodeListCategory>,
isShowingInfoPane: Binding<Bool>,
searchText: Binding<String>
) -> some View {
self.modifier(MainToolbarModifier(category: category, searchText: searchText))
self.modifier(
MainToolbarModifier(
category: category,
isShowingInfoPane: isShowingInfoPane,
searchText: searchText
)
)
}
}

View file

@ -0,0 +1,15 @@
import Foundation
enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConvertible {
case all
case installed
var id: Self { self }
var description: String {
switch self {
case .all: return "All"
case .installed: return "Installed"
}
}
}

View file

@ -4,10 +4,13 @@ import PromiseKit
struct XcodeListView: View {
@EnvironmentObject var appState: AppState
@State private var selection: Xcode.ID?
@State private var searchText: String = ""
@AppStorage("lastUpdated") private var lastUpdated: Double?
@AppStorage("xcodeListCategory") private var category: Category = .all
private let searchText: String
private let category: XcodeListCategory
init(searchText: String, category: XcodeListCategory) {
self.searchText = searchText
self.category = category
}
var visibleXcodes: [Xcode] {
var xcodes: [Xcode]
@ -25,20 +28,6 @@ struct XcodeListView: View {
return xcodes
}
enum Category: String, CaseIterable, Identifiable, CustomStringConvertible {
case all
case installed
var id: Self { self }
var description: String {
switch self {
case .all: return "All"
case .installed: return "Installed"
}
}
}
var body: some View {
List(visibleXcodes, selection: $appState.selectedXcodeID) { xcode in
HStack {
@ -69,58 +58,17 @@ struct XcodeListView: View {
}
.contextMenu {
InstallButton(xcode: xcode)
Divider()
if xcode.installed {
SelectButton(xcode: xcode)
LaunchButton(xcode: xcode)
OpenButton(xcode: xcode)
RevealButton(xcode: xcode)
CopyPathButton(xcode: xcode)
}
}
}
.mainToolbar(category: $category, searchText: $searchText)
.navigationSubtitle(subtitleText)
.frame(minWidth: 200, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
.alert(item: $appState.error) { error in
Alert(title: Text(error.title),
message: Text(verbatim: error.message),
dismissButton: .default(Text("OK")))
}
/*
Removing this for now, because it's overriding the error alert that's being worked on above.
.alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in
Alert(title: Text("Uninstall Xcode \(xcode.description)?"),
message: Text("It will be moved to the Trash, but won't be emptied."),
primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: xcode.id) }),
secondaryButton: .cancel(Text("Cancel")))
}
**/
.sheet(isPresented: $appState.secondFactorData.isNotNil) {
secondFactorView(appState.secondFactorData!)
.environmentObject(appState)
}
}
@ViewBuilder
func secondFactorView(_ secondFactorData: AppState.SecondFactorData) -> some View {
switch secondFactorData.option {
case .codeSent:
SignIn2FAView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
case .smsSent(let trustedPhoneNumber):
SignInSMSView(isPresented: $appState.secondFactorData.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
case .smsPendingChoice:
SignInPhoneListView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
}
}
private var subtitleText: Text {
if let lastUpdated = lastUpdated.map(Date.init(timeIntervalSince1970:)) {
return Text("Updated at \(lastUpdated, style: .date) \(lastUpdated, style: .time)")
} else {
return Text("")
}
}
@ViewBuilder
@ -138,7 +86,7 @@ struct XcodeListView: View {
struct XcodeListView_Previews: PreviewProvider {
static var previews: some View {
Group {
XcodeListView()
XcodeListView(searchText: "", category: .all)
.environmentObject({ () -> AppState in
let a = AppState()
a.allXcodes = [
@ -153,11 +101,3 @@ struct XcodeListView_Previews: PreviewProvider {
.previewLayout(.sizeThatFits)
}
}
extension Optional {
/// Note that this is lossy when setting, so you can really only set it to nil, but this is sufficient for mapping `Binding<Item?>` to `Binding<Bool>` for Alerts, Popovers, etc.
var isNotNil: Bool {
get { self != nil }
set { self = newValue ? self : nil }
}
}

View file

@ -9,7 +9,7 @@ struct XcodesApp: App {
var body: some Scene {
WindowGroup("Xcodes") {
XcodeListView()
MainWindow()
.environmentObject(appState)
// This is intentionally used on a View, and not on a WindowGroup,
// so that it's triggered when an individual window's phase changes instead of all window phases.