mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Merge pull request #24 from RobotsAndPencils/list
Update existing Xcode list view functionality
This commit is contained in:
commit
018a9a75bf
8 changed files with 358 additions and 73 deletions
|
|
@ -8,10 +8,12 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EAA4EA259944450046AB8F /* ProgressButton.swift */; };
|
||||
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */; };
|
||||
CA9FF83F2594FBC000E47BAF /* Licenses.rtf in Resources */ = {isa = PBXBuildFile; fileRef = CA9FF83E2594FBC000E47BAF /* Licenses.rtf */; };
|
||||
|
|
@ -58,6 +60,8 @@
|
|||
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 */; };
|
||||
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */; };
|
||||
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDB942598FE96003DCC5A /* FocusedValues.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -72,11 +76,13 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
63EAA4EA259944450046AB8F /* ProgressButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressButton.swift; sourceTree = "<group>"; };
|
||||
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
CA735108257BF96D00EA9CF8 /* AttributedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedText.swift; sourceTree = "<group>"; };
|
||||
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+.swift"; sourceTree = "<group>"; };
|
||||
CA8FB5F8256E0F9400469DA5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
|
|
@ -127,6 +133,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -219,16 +227,20 @@
|
|||
CABFA9B22592EEEA00380FEE /* Entry+.swift */,
|
||||
CABFA9A92592EEE900380FEE /* Environment.swift */,
|
||||
CABFA9B82592EEEA00380FEE /* FileManager+.swift */,
|
||||
CAFBDB942598FE96003DCC5A /* FocusedValues.swift */,
|
||||
CABFA9AC2592EEE900380FEE /* Foundation.swift */,
|
||||
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
|
||||
CABFA9AE2592EEE900380FEE /* Path+.swift */,
|
||||
CABFA9B42592EEEA00380FEE /* Process.swift */,
|
||||
CABFA9B02592EEEA00380FEE /* Promise+.swift */,
|
||||
CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */,
|
||||
CABFA9AB2592EEE900380FEE /* URLRequest+Apple.swift */,
|
||||
CABFA9B32592EEEA00380FEE /* URLSession+Promise.swift */,
|
||||
CABFA9A82592EEE900380FEE /* Version+.swift */,
|
||||
CA9FF876259528CC00E47BAF /* Version+XcodeReleases.swift */,
|
||||
CABFA9A62592EEE900380FEE /* Version+Xcode.swift */,
|
||||
CA61A6DF259835580008926E /* Xcode.swift */,
|
||||
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */,
|
||||
);
|
||||
path = Backend;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -459,6 +471,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */,
|
||||
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */,
|
||||
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
|
||||
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
|
||||
CABFA9CA2592EEEA00380FEE /* AppState+Update.swift in Sources */,
|
||||
|
|
@ -473,15 +486,18 @@
|
|||
CABFA9C52592EEEA00380FEE /* FileManager+.swift in Sources */,
|
||||
CABFA9CD2592EEEA00380FEE /* Foundation.swift in Sources */,
|
||||
CA9FF8872595607900E47BAF /* InstalledXcode.swift in Sources */,
|
||||
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
|
||||
CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */,
|
||||
CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */,
|
||||
CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */,
|
||||
CABFA9CE2592EEEA00380FEE /* Version+Xcode.swift in Sources */,
|
||||
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */,
|
||||
CAA1CB49255A5C97003FD669 /* SignInSMSView.swift in Sources */,
|
||||
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
|
||||
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */,
|
||||
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
|
||||
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
|
||||
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */,
|
||||
CABFA9C22592EEEA00380FEE /* Promise+.swift in Sources */,
|
||||
CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */,
|
||||
CABFA9CF2592EEEA00380FEE /* Process.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import Path
|
|||
import LegibleError
|
||||
import KeychainAccess
|
||||
import SwiftUI
|
||||
import Version
|
||||
|
||||
class AppState: ObservableObject {
|
||||
private let client = AppleAPI.Client()
|
||||
|
|
@ -13,10 +14,10 @@ class AppState: ObservableObject {
|
|||
@Published var authenticationState: AuthenticationState = .unauthenticated
|
||||
@Published var availableXcodes: [AvailableXcode] = [] {
|
||||
willSet {
|
||||
updateAllVersions(newValue)
|
||||
updateAllXcodes(newValue)
|
||||
}
|
||||
}
|
||||
var allVersions: [XcodeVersion] = []
|
||||
var allXcodes: [Xcode] = []
|
||||
@Published var updatePublisher: AnyCancellable?
|
||||
var isUpdating: Bool { updatePublisher != nil }
|
||||
@Published var error: AlertContent?
|
||||
|
|
@ -24,6 +25,11 @@ class AppState: ObservableObject {
|
|||
@Published var presentingSignInAlert = false
|
||||
@Published var isProcessingAuthRequest = false
|
||||
@Published var secondFactorData: SecondFactorData?
|
||||
// Selected in the Xcode list, not in the xcode-select sense
|
||||
// This probably belongs as private @State in XcodeListView,
|
||||
// but we need it here instead so that it can be a focusedValue at the top level in XcodesApp instead of in a list row. The latter seems more like how the focusedValue API is supposed to work, but currently doesn't.
|
||||
@Published var selectedXcodeID: Xcode.ID?
|
||||
@Published var xcodeBeingConfirmedForUninstallation: Xcode?
|
||||
|
||||
init() {
|
||||
try? loadCachedAvailableXcodes()
|
||||
|
|
@ -171,27 +177,39 @@ class AppState: ObservableObject {
|
|||
|
||||
// MARK: -
|
||||
|
||||
func install(id: String) {
|
||||
func install(id: Xcode.ID) {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
func uninstall(id: String) {
|
||||
func uninstall(id: Xcode.ID) {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
func reveal(id: String) {
|
||||
func reveal(id: Xcode.ID) {
|
||||
// TODO: show error if not
|
||||
guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version.xcodeDescription == id }) else { return }
|
||||
guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }) else { return }
|
||||
NSWorkspace.shared.activateFileViewerSelecting([installedXcode.path.url])
|
||||
}
|
||||
|
||||
func select(id: String) {
|
||||
func select(id: Xcode.ID) {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
func launch(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())
|
||||
}
|
||||
|
||||
func copyPath(id: Xcode.ID) {
|
||||
guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }) else { return }
|
||||
NSPasteboard.general.declareTypes([.URL, .string], owner: nil)
|
||||
NSPasteboard.general.writeObjects([installedXcode.path.url as NSURL])
|
||||
NSPasteboard.general.setString(installedXcode.path.string, forType: .string)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func updateAllVersions(_ xcodes: [AvailableXcode]) {
|
||||
private func updateAllXcodes(_ xcodes: [AvailableXcode]) {
|
||||
let installedXcodes = Current.files.installedXcodes(Path.root/"Applications")
|
||||
var allXcodeVersions = xcodes.map { $0.version }
|
||||
for installedXcode in installedXcodes {
|
||||
|
|
@ -210,37 +228,22 @@ class AppState: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
allVersions = allXcodeVersions
|
||||
allXcodes = allXcodeVersions
|
||||
.sorted(by: >)
|
||||
.map { xcodeVersion in
|
||||
let installedXcode = installedXcodes.first(where: { xcodeVersion.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) })
|
||||
return XcodeVersion(
|
||||
title: xcodeVersion.xcodeDescription,
|
||||
return Xcode(
|
||||
version: xcodeVersion,
|
||||
installState: installedXcodes.contains(where: { xcodeVersion.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) }) ? .installed : .notInstalled,
|
||||
selected: false,
|
||||
path: installedXcode?.path.string
|
||||
path: installedXcode?.path.string,
|
||||
icon: (installedXcode?.path.string).map(NSWorkspace.shared.icon(forFile:))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Nested Types
|
||||
|
||||
/// A merging of AvailableXcode and InstalledXcode prepared for display
|
||||
struct XcodeVersion: Identifiable {
|
||||
let title: String
|
||||
let installState: InstallState
|
||||
let selected: Bool
|
||||
let path: String?
|
||||
var id: String { title }
|
||||
var installed: Bool { installState == .installed }
|
||||
}
|
||||
|
||||
enum InstallState: Equatable {
|
||||
case notInstalled
|
||||
case installing(Progress)
|
||||
case installed
|
||||
}
|
||||
|
||||
struct AlertContent: Identifiable {
|
||||
var title: String
|
||||
|
|
|
|||
16
Xcodes/Backend/FocusedValues.swift
Normal file
16
Xcodes/Backend/FocusedValues.swift
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import SwiftUI
|
||||
|
||||
// MARK: - FocusedXcodeKey
|
||||
|
||||
struct FocusedXcodeKey : FocusedValueKey {
|
||||
typealias Value = SelectedXcode
|
||||
}
|
||||
|
||||
// MARK: - FocusedValues
|
||||
|
||||
extension FocusedValues {
|
||||
var selectedXcode: FocusedXcodeKey.Value? {
|
||||
get { self[FocusedXcodeKey.self] }
|
||||
set { self[FocusedXcodeKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
35
Xcodes/Backend/SelectedXcode.swift
Normal file
35
Xcodes/Backend/SelectedXcode.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import Foundation
|
||||
|
||||
/// As part of the unexpected way we have to use focusedValue in XcodesApp, we need to provide an `Optional<Xcode>` because there isn't always a selected Xcode in the focused window.
|
||||
/// But FocusedValueKey.Value is already optional, because there might not be a focused UI element to begin with, so the type ends up being `Optional<Optional<Xcode>>`.
|
||||
/// This is weird enough, but I wasn't able to find a way to have FocusedXcodeKey.Value be `Optional<Optional<Xcode>>` and still compile.
|
||||
/// There was always an error somewhere in either the use of @FocusedValue or FocusedValues.xcode or .focusedValue, as if it is only ever expecting a single level of optionality.
|
||||
/// But! If we make our own Optional replica like SelectedXcode, it _does_ compile, and there's some more noise required to turn it back into an `Optional<Xcode>`.
|
||||
/// All this to say, maybe one day we don't need to have this type at all.
|
||||
enum SelectedXcode {
|
||||
case none
|
||||
case some(Xcode)
|
||||
|
||||
init(_ optional: Optional<Xcode>) {
|
||||
switch optional {
|
||||
case .none: self = .none
|
||||
case let .some(xcode): self = .some(xcode)
|
||||
}
|
||||
}
|
||||
|
||||
var asOptional: Xcode? {
|
||||
switch self {
|
||||
case .none: return .none
|
||||
case let .some(xcode): return .some(xcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional where Wrapped == SelectedXcode {
|
||||
var unwrapped: Xcode? {
|
||||
switch self {
|
||||
case Optional<SelectedXcode>.none: return Optional<Xcode>.none
|
||||
case let .some(selectedXcode): return selectedXcode.asOptional
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Xcodes/Backend/Xcode.swift
Normal file
24
Xcodes/Backend/Xcode.swift
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import AppKit
|
||||
import Foundation
|
||||
import Version
|
||||
|
||||
struct Xcode: Identifiable, CustomStringConvertible {
|
||||
let version: Version
|
||||
let installState: XcodeInstallState
|
||||
let selected: Bool
|
||||
let path: String?
|
||||
let icon: NSImage?
|
||||
|
||||
var id: Version { version }
|
||||
var installed: Bool { installState == .installed }
|
||||
|
||||
var description: String {
|
||||
version.xcodeDescription
|
||||
}
|
||||
}
|
||||
|
||||
enum XcodeInstallState: Equatable {
|
||||
case notInstalled
|
||||
case installing(Progress)
|
||||
case installed
|
||||
}
|
||||
172
Xcodes/Backend/XcodeCommands.swift
Normal file
172
Xcodes/Backend/XcodeCommands.swift
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import SwiftUI
|
||||
|
||||
// MARK: - CommandMenu
|
||||
|
||||
struct XcodeCommands: Commands {
|
||||
// CommandMenus don't participate in the environment hierarchy, so we need to shuffle AppState along to the individual Commands manually.
|
||||
let appState: AppState
|
||||
|
||||
var body: some Commands {
|
||||
CommandMenu("Xcode") {
|
||||
Group {
|
||||
InstallCommand()
|
||||
|
||||
Divider()
|
||||
|
||||
SelectCommand()
|
||||
LaunchCommand()
|
||||
RevealCommand()
|
||||
CopyPathCommand()
|
||||
}
|
||||
.environmentObject(appState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Buttons
|
||||
// These are used for both context menus and commands
|
||||
|
||||
struct InstallButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
Button(action: uninstallOrInstall) {
|
||||
if let xcode = xcode {
|
||||
Text(xcode.installed == true ? "Uninstall" : "Install")
|
||||
} else {
|
||||
Text("Install")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func uninstallOrInstall() {
|
||||
guard let xcode = xcode else { return }
|
||||
if xcode.installed {
|
||||
appState.xcodeBeingConfirmedForUninstallation = xcode
|
||||
} else {
|
||||
appState.install(id: xcode.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
Button(action: select) {
|
||||
Text("Select")
|
||||
}
|
||||
}
|
||||
|
||||
private func select() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.select(id: xcode.id)
|
||||
}
|
||||
}
|
||||
|
||||
struct LaunchButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
Button(action: launch) {
|
||||
Text("Launch")
|
||||
}
|
||||
}
|
||||
|
||||
private func launch() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.launch(id: xcode.id)
|
||||
}
|
||||
}
|
||||
|
||||
struct RevealButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
Button(action: reveal) {
|
||||
Text("Reveal in Finder")
|
||||
}
|
||||
}
|
||||
|
||||
private func reveal() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.reveal(id: xcode.id)
|
||||
}
|
||||
}
|
||||
|
||||
struct CopyPathButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
Button(action: copyPath) {
|
||||
Text("Copy Path")
|
||||
}
|
||||
}
|
||||
|
||||
private func copyPath() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.copyPath(id: xcode.id)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Commands
|
||||
|
||||
struct InstallCommand: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
|
||||
|
||||
var body: some View {
|
||||
InstallButton(xcode: selectedXcode.unwrapped)
|
||||
.keyboardShortcut(selectedXcode.unwrapped?.installed == true ? "u" : "i", modifiers: [.command, .option])
|
||||
.disabled(selectedXcode.unwrapped == nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectCommand: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
|
||||
|
||||
var body: some View {
|
||||
SelectButton(xcode: selectedXcode.unwrapped)
|
||||
.keyboardShortcut("s", modifiers: [.command, .option])
|
||||
.disabled(selectedXcode.unwrapped?.installed != true)
|
||||
}
|
||||
}
|
||||
|
||||
struct LaunchCommand: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
|
||||
|
||||
var body: some View {
|
||||
LaunchButton(xcode: selectedXcode.unwrapped)
|
||||
.keyboardShortcut(KeyboardShortcut(.downArrow, modifiers: .command))
|
||||
.disabled(selectedXcode.unwrapped?.installed != true)
|
||||
}
|
||||
}
|
||||
|
||||
struct RevealCommand: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
|
||||
|
||||
var body: some View {
|
||||
RevealButton(xcode: selectedXcode.unwrapped)
|
||||
.keyboardShortcut("r", modifiers: [.command, .option])
|
||||
.disabled(selectedXcode.unwrapped?.installed != true)
|
||||
}
|
||||
}
|
||||
|
||||
struct CopyPathCommand: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
|
||||
|
||||
var body: some View {
|
||||
CopyPathButton(xcode: selectedXcode.unwrapped)
|
||||
.keyboardShortcut("c", modifiers: [.command, .option])
|
||||
.disabled(selectedXcode.unwrapped?.installed != true)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,27 +4,25 @@ import PromiseKit
|
|||
|
||||
struct XcodeListView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var selection = Set<String>()
|
||||
@State private var rowBeingConfirmedForUninstallation: AppState.XcodeVersion?
|
||||
@State private var selection: Xcode.ID?
|
||||
@State private var searchText: String = ""
|
||||
@AppStorage("lastUpdated") private var lastUpdated: Double?
|
||||
|
||||
@AppStorage("xcodeListCategory") private var category: Category = .all
|
||||
|
||||
var visibleVersions: [AppState.XcodeVersion] {
|
||||
var versions: [AppState.XcodeVersion]
|
||||
var visibleXcodes: [Xcode] {
|
||||
var xcodes: [Xcode]
|
||||
switch category {
|
||||
case .all:
|
||||
versions = appState.allVersions
|
||||
xcodes = appState.allXcodes
|
||||
case .installed:
|
||||
versions = appState.allVersions.filter { $0.installed }
|
||||
xcodes = appState.allXcodes.filter { $0.installed }
|
||||
}
|
||||
|
||||
if !searchText.isEmpty {
|
||||
versions = versions.filter { $0.title.contains(searchText) }
|
||||
xcodes = xcodes.filter { $0.description.contains(searchText) }
|
||||
}
|
||||
|
||||
return versions
|
||||
return xcodes
|
||||
}
|
||||
|
||||
enum Category: String, CaseIterable, Identifiable, CustomStringConvertible {
|
||||
|
|
@ -42,38 +40,43 @@ struct XcodeListView: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
List(visibleVersions, selection: $selection) { row in
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(row.title)
|
||||
List(visibleXcodes, selection: $appState.selectedXcodeID) { xcode in
|
||||
HStack {
|
||||
appIconView(for: xcode)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(xcode.description)
|
||||
.font(.body)
|
||||
if row.selected {
|
||||
Tag(text: "SELECTED")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
Spacer()
|
||||
Button(row.installed ? "INSTALLED" : "INSTALL") {
|
||||
print("Installing...")
|
||||
}
|
||||
.buttonStyle(AppStoreButtonStyle(installed: row.installed,
|
||||
highlighted: self.selection.contains(row.id)))
|
||||
.disabled(row.installed)
|
||||
|
||||
Text(verbatim: xcode.path ?? "")
|
||||
.font(.caption)
|
||||
.foregroundColor(appState.selectedXcodeID == xcode.id ? Color(NSColor.selectedMenuItemTextColor) : Color(NSColor.secondaryLabelColor))
|
||||
}
|
||||
Text(verbatim: row.path ?? "")
|
||||
.font(.caption)
|
||||
.foregroundColor(self.selection.contains(row.id) ? Color(NSColor.selectedMenuItemTextColor) : Color(NSColor.secondaryLabelColor))
|
||||
|
||||
if xcode.selected {
|
||||
Tag(text: "SELECTED")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(xcode.installed ? "INSTALLED" : "INSTALL") {
|
||||
print("Installing...")
|
||||
}
|
||||
.buttonStyle(AppStoreButtonStyle(installed: xcode.installed,
|
||||
highlighted: appState.selectedXcodeID == xcode.id))
|
||||
.disabled(xcode.installed)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: { row.installed ? self.rowBeingConfirmedForUninstallation = row : self.appState.install(id: row.id) }) {
|
||||
Text(row.installed ? "Uninstall" : "Install")
|
||||
}
|
||||
if row.installed {
|
||||
Button(action: { self.appState.reveal(id: row.id) }) {
|
||||
Text("Reveal in Finder")
|
||||
}
|
||||
Button(action: { self.appState.select(id: row.id) }) {
|
||||
Text("Select")
|
||||
}
|
||||
InstallButton(xcode: xcode)
|
||||
|
||||
Divider()
|
||||
|
||||
if xcode.installed {
|
||||
SelectButton(xcode: xcode)
|
||||
LaunchButton(xcode: xcode)
|
||||
RevealButton(xcode: xcode)
|
||||
CopyPathButton(xcode: xcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -114,10 +117,10 @@ struct XcodeListView: View {
|
|||
}
|
||||
/*
|
||||
Removing this for now, because it's overriding the error alert that's being worked on above.
|
||||
.alert(item: self.$rowBeingConfirmedForUninstallation) { row in
|
||||
Alert(title: Text("Uninstall Xcode \(row.title)?"),
|
||||
.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: row.id) }),
|
||||
primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: xcode.id) }),
|
||||
secondaryButton: .cancel(Text("Cancel")))
|
||||
}
|
||||
**/
|
||||
|
|
@ -146,6 +149,17 @@ struct XcodeListView: View {
|
|||
return Text("")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func appIconView(for xcode: Xcode) -> some View {
|
||||
if let icon = xcode.icon {
|
||||
Image(nsImage: icon)
|
||||
} else {
|
||||
Color.clear
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct XcodeListView_Previews: PreviewProvider {
|
||||
|
|
@ -154,11 +168,11 @@ struct XcodeListView_Previews: PreviewProvider {
|
|||
XcodeListView()
|
||||
.environmentObject({ () -> AppState in
|
||||
let a = AppState()
|
||||
a.allVersions = [
|
||||
AppState.XcodeVersion(title: "12.3", installState: .installed, selected: true, path: nil),
|
||||
AppState.XcodeVersion(title: "12.2", installState: .notInstalled, selected: false, path: nil),
|
||||
AppState.XcodeVersion(title: "12.1", installState: .notInstalled, selected: false, path: nil),
|
||||
AppState.XcodeVersion(title: "12.0", installState: .installed, selected: false, path: nil),
|
||||
a.allXcodes = [
|
||||
Xcode(version: Version("12.3.0")!, installState: .installed, selected: true, path: nil, icon: nil),
|
||||
Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, path: nil, icon: nil),
|
||||
Xcode(version: Version("12.1.0")!, installState: .notInstalled, selected: false, path: nil, icon: nil),
|
||||
Xcode(version: Version("12.0.0")!, installState: .installed, selected: false, path: nil, icon: nil),
|
||||
]
|
||||
return a
|
||||
}())
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ struct XcodesApp: App {
|
|||
appState.updateIfNeeded()
|
||||
}
|
||||
}
|
||||
// I'm expecting to be able to use this modifier on a List row, but using it at the top level here is the only way that has made XcodeCommands work so far.
|
||||
// FB8954571 focusedValue(_:_:) on List row doesn't propagate value to @FocusedValue
|
||||
.focusedValue(\.selectedXcode, SelectedXcode(appState.allXcodes.first { $0.id == appState.selectedXcodeID }))
|
||||
}
|
||||
.commands {
|
||||
CommandGroup(replacing: .appInfo) {
|
||||
|
|
@ -34,6 +37,8 @@ struct XcodesApp: App {
|
|||
.keyboardShortcut(KeyEquivalent("r"))
|
||||
.disabled(appState.isUpdating)
|
||||
}
|
||||
|
||||
XcodeCommands(appState: appState)
|
||||
}
|
||||
|
||||
Settings {
|
||||
|
|
|
|||
Loading…
Reference in a new issue