mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Merge branch 'main' into enhancement/31-helpModifier
This commit is contained in:
commit
1cd96cc9fc
9 changed files with 109 additions and 42 deletions
|
|
@ -15,13 +15,25 @@ extension AppState {
|
|||
let lastUpdated = Current.defaults.date(forKey: "lastUpdated"),
|
||||
// This is bad date math but for this use case it doesn't need to be exact
|
||||
lastUpdated < Current.date().addingTimeInterval(-60 * 60 * 24)
|
||||
else { return }
|
||||
else {
|
||||
updatePublisher = updateSelectedXcodePath()
|
||||
.sink(
|
||||
receiveCompletion: { _ in
|
||||
self.updatePublisher = nil
|
||||
},
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
return
|
||||
}
|
||||
update() as Void
|
||||
}
|
||||
|
||||
func update() {
|
||||
guard !isUpdating else { return }
|
||||
updatePublisher = updateAvailableXcodes(from: self.dataSource)
|
||||
updatePublisher = updateSelectedXcodePath()
|
||||
.flatMap { _ in
|
||||
self.updateAvailableXcodes(from: self.dataSource)
|
||||
}
|
||||
.sink(
|
||||
receiveCompletion: { [unowned self] completion in
|
||||
switch completion {
|
||||
|
|
@ -36,6 +48,15 @@ extension AppState {
|
|||
receiveValue: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
func updateSelectedXcodePath() -> AnyPublisher<Void, Never> {
|
||||
Current.shell.xcodeSelectPrintPath()
|
||||
.handleEvents(receiveOutput: { output in self.selectedXcodePath = output.out })
|
||||
// Ignore xcode-select failures
|
||||
.map { _ in Void() }
|
||||
.catch { _ in Just(()) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private func updateAvailableXcodes(from dataSource: DataSource) -> AnyPublisher<[AvailableXcode], Error> {
|
||||
switch dataSource {
|
||||
|
|
|
|||
|
|
@ -16,10 +16,15 @@ class AppState: ObservableObject {
|
|||
@Published var authenticationState: AuthenticationState = .unauthenticated
|
||||
@Published var availableXcodes: [AvailableXcode] = [] {
|
||||
willSet {
|
||||
updateAllXcodes(newValue)
|
||||
updateAllXcodes(availableXcodes: newValue, selectedXcodePath: selectedXcodePath)
|
||||
}
|
||||
}
|
||||
var allXcodes: [Xcode] = []
|
||||
@Published var selectedXcodePath: String? {
|
||||
willSet {
|
||||
updateAllXcodes(availableXcodes: availableXcodes, selectedXcodePath: newValue)
|
||||
}
|
||||
}
|
||||
@Published var updatePublisher: AnyCancellable?
|
||||
var isUpdating: Bool { updatePublisher != nil }
|
||||
@Published var error: AlertContent?
|
||||
|
|
@ -27,10 +32,6 @@ 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?
|
||||
@Published var helperInstallState: HelperInstallState = .notInstalled
|
||||
|
||||
|
|
@ -226,6 +227,9 @@ class AppState: ObservableObject {
|
|||
else { return }
|
||||
|
||||
selectPublisher = HelperClient().switchXcodePath(installedXcode.path.string)
|
||||
.flatMap { [unowned self] _ in
|
||||
self.updateSelectedXcodePath()
|
||||
}
|
||||
.sink(
|
||||
receiveCompletion: { [unowned self] completion in
|
||||
if case let .failure(error) = completion {
|
||||
|
|
@ -251,9 +255,9 @@ class AppState: ObservableObject {
|
|||
|
||||
// MARK: - Private
|
||||
|
||||
private func updateAllXcodes(_ xcodes: [AvailableXcode]) {
|
||||
private func updateAllXcodes(availableXcodes: [AvailableXcode], selectedXcodePath: String?) {
|
||||
let installedXcodes = Current.files.installedXcodes(Path.root/"Applications")
|
||||
var allXcodeVersions = xcodes.map { $0.version }
|
||||
var allXcodeVersions = availableXcodes.map { $0.version }
|
||||
for installedXcode in installedXcodes {
|
||||
// If an installed version isn't listed online, add the installed version
|
||||
if !allXcodeVersions.contains(where: { version in
|
||||
|
|
@ -274,11 +278,11 @@ 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 }
|
||||
let availableXcode = availableXcodes.first { $0.version == xcodeVersion }
|
||||
return Xcode(
|
||||
version: xcodeVersion,
|
||||
installState: installedXcodes.contains(where: { xcodeVersion.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) }) ? .installed : .notInstalled,
|
||||
selected: false,
|
||||
installState: installedXcode != nil ? .installed : .notInstalled,
|
||||
selected: installedXcode != nil && selectedXcodePath?.hasPrefix(installedXcode!.path.string) == true,
|
||||
path: installedXcode?.path.string,
|
||||
icon: (installedXcode?.path.string).map(NSWorkspace.shared.icon(forFile:)),
|
||||
requiredMacOSVersion: availableXcode?.requiredMacOSVersion,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import PMKFoundation
|
||||
|
|
@ -50,11 +51,7 @@ public struct Shell {
|
|||
authenticateSudoerIfNecessary(passwordInput)
|
||||
}
|
||||
|
||||
public var xcodeSelectPrintPath: () -> Promise<ProcessOutput> = { Process.run(Path.root.usr.bin.join("xcode-select"), "-p") }
|
||||
public var xcodeSelectSwitch: (String?, String) -> Promise<ProcessOutput> = { Process.sudo(password: $0, Path.root.usr.bin.join("xcode-select"), "-s", $1) }
|
||||
public func xcodeSelectSwitch(password: String?, path: String) -> Promise<ProcessOutput> {
|
||||
xcodeSelectSwitch(password, path)
|
||||
}
|
||||
public var xcodeSelectPrintPath: () -> AnyPublisher<ProcessOutput, Error> = { Process.run(Path.root.usr.bin.join("xcode-select"), "-p") }
|
||||
}
|
||||
|
||||
public struct Files {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import PMKFoundation
|
||||
|
|
@ -38,4 +39,47 @@ extension Process {
|
|||
return (process.terminationStatus, output, error)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func run(_ executable: Path, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) -> AnyPublisher<ProcessOutput, Error> {
|
||||
return run(executable.url, workingDirectory: workingDirectory, input: input, arguments)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func run(_ executable: URL, workingDirectory: URL? = nil, input: String? = nil, _ arguments: [String]) -> AnyPublisher<ProcessOutput, Error> {
|
||||
Deferred {
|
||||
Future<ProcessOutput, Error> { promise in
|
||||
let process = Process()
|
||||
process.currentDirectoryURL = workingDirectory ?? executable.deletingLastPathComponent()
|
||||
process.executableURL = executable
|
||||
process.arguments = arguments
|
||||
|
||||
let (stdout, stderr) = (Pipe(), Pipe())
|
||||
process.standardOutput = stdout
|
||||
process.standardError = stderr
|
||||
|
||||
if let input = input {
|
||||
let inputPipe = Pipe()
|
||||
process.standardInput = inputPipe.fileHandleForReading
|
||||
inputPipe.fileHandleForWriting.write(Data(input.utf8))
|
||||
inputPipe.fileHandleForWriting.closeFile()
|
||||
}
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
let output = String(data: stdout.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
||||
let error = String(data: stderr.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
||||
|
||||
promise(.success((process.terminationStatus, output, error)))
|
||||
} catch {
|
||||
promise(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscribe(on: DispatchQueue.global())
|
||||
.receive(on: DispatchQueue.main)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ struct SelectButton: View {
|
|||
Button(action: select) {
|
||||
Text("Select")
|
||||
}
|
||||
.disabled(xcode?.selected != false)
|
||||
.help("Select")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import SwiftUI
|
|||
|
||||
struct MainWindow: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var selection: Xcode.ID?
|
||||
@State private var selectedXcodeID: Xcode.ID?
|
||||
@State private var searchText: String = ""
|
||||
@AppStorage("lastUpdated") private var lastUpdated: Double?
|
||||
@SceneStorage("isShowingInfoPane") private var isShowingInfoPane = false
|
||||
|
|
@ -10,11 +10,11 @@ struct MainWindow: View {
|
|||
|
||||
var body: some View {
|
||||
HSplitView {
|
||||
XcodeListView(searchText: searchText, category: category)
|
||||
XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category)
|
||||
.frame(minWidth: 300)
|
||||
.layoutPriority(1)
|
||||
|
||||
InspectorPane()
|
||||
InspectorPane(selectedXcodeID: selectedXcodeID)
|
||||
.frame(minWidth: 300, maxWidth: .infinity)
|
||||
.frame(width: isShowingInfoPane ? nil : 0)
|
||||
.isHidden(!isShowingInfoPane)
|
||||
|
|
@ -44,6 +44,9 @@ struct MainWindow: View {
|
|||
secondFactorView(appState.secondFactorData!)
|
||||
.environmentObject(appState)
|
||||
}
|
||||
// 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 == selectedXcodeID }))
|
||||
}
|
||||
|
||||
private var subtitleText: Text {
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ import struct XCModel.Compilers
|
|||
|
||||
struct InspectorPane: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let selectedXcodeID: Xcode.ID?
|
||||
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let xcode = appState.allXcodes.first(where: { $0.id == appState.selectedXcodeID }) {
|
||||
if let xcode = appState.allXcodes.first(where: { $0.id == selectedXcodeID }) {
|
||||
VStack(spacing: 16) {
|
||||
icon(for: xcode)
|
||||
|
||||
|
|
@ -172,7 +173,7 @@ struct InspectorPane: View {
|
|||
struct InspectorPane_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
InspectorPane()
|
||||
InspectorPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
.init(
|
||||
|
|
@ -197,11 +198,10 @@ struct InspectorPane_Previews: PreviewProvider {
|
|||
swift: .init(number: "5.3.2")
|
||||
))
|
||||
]
|
||||
$0.selectedXcodeID = Version(major: 12, minor: 3, patch: 0)
|
||||
})
|
||||
.previewDisplayName("Populated, Installed, Selected")
|
||||
|
||||
InspectorPane()
|
||||
InspectorPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
.init(
|
||||
|
|
@ -224,11 +224,10 @@ struct InspectorPane_Previews: PreviewProvider {
|
|||
swift: .init(number: "5.3.2")
|
||||
))
|
||||
]
|
||||
$0.selectedXcodeID = Version(major: 12, minor: 3, patch: 0)
|
||||
})
|
||||
.previewDisplayName("Populated, Installed, Unselected")
|
||||
|
||||
InspectorPane()
|
||||
InspectorPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
.init(
|
||||
|
|
@ -251,15 +250,13 @@ struct InspectorPane_Previews: PreviewProvider {
|
|||
swift: .init(number: "5.3.2")
|
||||
))
|
||||
]
|
||||
$0.selectedXcodeID = Version(major: 12, minor: 3, patch: 0)
|
||||
})
|
||||
.previewDisplayName("Populated, Uninstalled")
|
||||
|
||||
InspectorPane()
|
||||
InspectorPane(selectedXcodeID: nil)
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
]
|
||||
$0.selectedXcodeID = nil
|
||||
})
|
||||
.previewDisplayName("Empty")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import PromiseKit
|
|||
|
||||
struct XcodeListView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@Binding var selectedXcodeID: Xcode.ID?
|
||||
private let searchText: String
|
||||
private let category: XcodeListCategory
|
||||
|
||||
init(searchText: String, category: XcodeListCategory) {
|
||||
init(selectedXcodeID: Binding<Xcode.ID?>, searchText: String, category: XcodeListCategory) {
|
||||
self._selectedXcodeID = selectedXcodeID
|
||||
self.searchText = searchText
|
||||
self.category = category
|
||||
}
|
||||
|
|
@ -29,7 +31,7 @@ struct XcodeListView: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
List(visibleXcodes, selection: $appState.selectedXcodeID) { xcode in
|
||||
List(visibleXcodes, selection: $selectedXcodeID) { xcode in
|
||||
HStack {
|
||||
appIconView(for: xcode)
|
||||
|
||||
|
|
@ -39,22 +41,23 @@ struct XcodeListView: View {
|
|||
|
||||
Text(verbatim: xcode.path ?? "")
|
||||
.font(.caption)
|
||||
.foregroundColor(appState.selectedXcodeID == xcode.id ? Color(NSColor.selectedMenuItemTextColor) : Color(NSColor.secondaryLabelColor))
|
||||
.foregroundColor(selectedXcodeID == xcode.id ? Color(NSColor.selectedMenuItemTextColor) : Color(NSColor.secondaryLabelColor))
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
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)
|
||||
highlighted: selectedXcodeID == xcode.id))
|
||||
.disabled(xcode.installed)
|
||||
}
|
||||
.contextMenu {
|
||||
InstallButton(xcode: xcode)
|
||||
|
|
@ -86,14 +89,14 @@ struct XcodeListView: View {
|
|||
struct XcodeListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
XcodeListView(searchText: "", category: .all)
|
||||
XcodeListView(selectedXcodeID: .constant(nil), searchText: "", category: .all)
|
||||
.environmentObject({ () -> AppState in
|
||||
let a = AppState()
|
||||
a.allXcodes = [
|
||||
Xcode(version: Version("12.3.0")!, installState: .installed, selected: true, path: nil, icon: nil),
|
||||
Xcode(version: Version("12.3.0")!, installState: .installed, selected: true, path: "/Applications/Xcode-12.3.0.app", 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),
|
||||
Xcode(version: Version("12.0.0")!, installState: .installed, selected: false, path: "/Applications/Xcode-12.3.0.app", icon: nil),
|
||||
]
|
||||
return a
|
||||
}())
|
||||
|
|
|
|||
|
|
@ -20,9 +20,6 @@ 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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue