mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Merge branch 'main' into matt/runtimeDownload
This commit is contained in:
commit
f470986e2b
18 changed files with 743 additions and 460 deletions
|
|
@ -9,10 +9,21 @@
|
|||
/* Begin PBXBuildFile section */
|
||||
36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */; };
|
||||
36741BFF291E50F500A85AAE /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFE291E50F500A85AAE /* FileError.swift */; };
|
||||
536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; };
|
||||
536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; };
|
||||
536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */; };
|
||||
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CBAB2B263DCC9100410495 /* XcodesAlert.swift */; };
|
||||
63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EAA4EA259944450046AB8F /* ProgressButton.swift */; };
|
||||
B0403CF02AD92D7B00137C09 /* ReleaseNotesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0403CEF2AD92D7B00137C09 /* ReleaseNotesView.swift */; };
|
||||
B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0403CF12AD934B600137C09 /* CompatibilityView.swift */; };
|
||||
B0403CF42AD9381D00137C09 /* SDKsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0403CF32AD9381D00137C09 /* SDKsView.swift */; };
|
||||
B0403CF62AD9849E00137C09 /* CompilersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0403CF52AD9849E00137C09 /* CompilersView.swift */; };
|
||||
B0403CF82AD991F800137C09 /* UnselectedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0403CF72AD991F800137C09 /* UnselectedView.swift */; };
|
||||
B0403CFA2AD9942A00137C09 /* NotInstalledStateButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0403CF92AD9942A00137C09 /* NotInstalledStateButtons.swift */; };
|
||||
B0403CFC2AD9A6BF00137C09 /* InstalledStateButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0403CFB2AD9A6BF00137C09 /* InstalledStateButtons.swift */; };
|
||||
B0403CFE2ADA712C00137C09 /* InfoPaneControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0403CFD2ADA712C00137C09 /* InfoPaneControls.swift */; };
|
||||
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0C6AD032AD6E65700E64698 /* ReleaseDateView.swift */; };
|
||||
B0C6AD0B2AD9178E00E64698 /* IdenticalBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0C6AD0A2AD9178E00E64698 /* IdenticalBuildView.swift */; };
|
||||
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0C6AD0C2AD91D7900E64698 /* IconView.swift */; };
|
||||
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */; };
|
||||
CA2518EC25A7FF2B00F08414 /* AppStateUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */; };
|
||||
CA25192A25A9644800F08414 /* XcodeInstallState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA25192925A9644800F08414 /* XcodeInstallState.swift */; };
|
||||
|
|
@ -195,6 +206,17 @@
|
|||
A0187D39285792D1002F46F9 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
AAB037D32839BD4700017680 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
AB4EB0DE28541FA000FF3B1D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
B0403CEF2AD92D7B00137C09 /* ReleaseNotesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseNotesView.swift; sourceTree = "<group>"; };
|
||||
B0403CF12AD934B600137C09 /* CompatibilityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibilityView.swift; sourceTree = "<group>"; };
|
||||
B0403CF32AD9381D00137C09 /* SDKsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKsView.swift; sourceTree = "<group>"; };
|
||||
B0403CF52AD9849E00137C09 /* CompilersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompilersView.swift; sourceTree = "<group>"; };
|
||||
B0403CF72AD991F800137C09 /* UnselectedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnselectedView.swift; sourceTree = "<group>"; };
|
||||
B0403CF92AD9942A00137C09 /* NotInstalledStateButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotInstalledStateButtons.swift; sourceTree = "<group>"; };
|
||||
B0403CFB2AD9A6BF00137C09 /* InstalledStateButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledStateButtons.swift; sourceTree = "<group>"; };
|
||||
B0403CFD2ADA712C00137C09 /* InfoPaneControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPaneControls.swift; sourceTree = "<group>"; };
|
||||
B0C6AD032AD6E65700E64698 /* ReleaseDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseDateView.swift; sourceTree = "<group>"; };
|
||||
B0C6AD0A2AD9178E00E64698 /* IdenticalBuildView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdenticalBuildView.swift; sourceTree = "<group>"; };
|
||||
B0C6AD0C2AD91D7900E64698 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = "<group>"; };
|
||||
B648F22B2810C1130096781B /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
C0AE7FA4283002DC00DA63D2 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -623,8 +645,19 @@
|
|||
E8E98A9425D863B100EC89A0 /* InfoPane */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B0403CEF2AD92D7B00137C09 /* ReleaseNotesView.swift */,
|
||||
B0403CF32AD9381D00137C09 /* SDKsView.swift */,
|
||||
B0403CF52AD9849E00137C09 /* CompilersView.swift */,
|
||||
B0403CF12AD934B600137C09 /* CompatibilityView.swift */,
|
||||
CAFBDC67259A308B003DCC5A /* InfoPane.swift */,
|
||||
B0403CFD2ADA712C00137C09 /* InfoPaneControls.swift */,
|
||||
B0403CF72AD991F800137C09 /* UnselectedView.swift */,
|
||||
B0403CF92AD9942A00137C09 /* NotInstalledStateButtons.swift */,
|
||||
B0403CFB2AD9A6BF00137C09 /* InstalledStateButtons.swift */,
|
||||
E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */,
|
||||
B0C6AD032AD6E65700E64698 /* ReleaseDateView.swift */,
|
||||
B0C6AD0A2AD9178E00E64698 /* IdenticalBuildView.swift */,
|
||||
B0C6AD0C2AD91D7900E64698 /* IconView.swift */,
|
||||
E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */,
|
||||
);
|
||||
path = InfoPane;
|
||||
|
|
@ -853,7 +886,9 @@
|
|||
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
|
||||
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
|
||||
CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */,
|
||||
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */,
|
||||
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
|
||||
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */,
|
||||
CAA8587C25A2B37900ACF8C0 /* IsTesting.swift in Sources */,
|
||||
CABFA9CA2592EEEA00380FEE /* AppState+Update.swift in Sources */,
|
||||
CA44901F2463AD34003D8213 /* Tag.swift in Sources */,
|
||||
|
|
@ -870,12 +905,16 @@
|
|||
CAA1CB45255A5B60003FD669 /* SignIn2FAView.swift in Sources */,
|
||||
CABFA9C52592EEEA00380FEE /* FileManager+.swift in Sources */,
|
||||
CABFA9CD2592EEEA00380FEE /* Foundation.swift in Sources */,
|
||||
B0403CFC2AD9A6BF00137C09 /* InstalledStateButtons.swift in Sources */,
|
||||
36741BFF291E50F500A85AAE /* FileError.swift in Sources */,
|
||||
CA9FF8872595607900E47BAF /* InstalledXcode.swift in Sources */,
|
||||
B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */,
|
||||
B0403CFE2ADA712C00137C09 /* InfoPaneControls.swift in Sources */,
|
||||
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */,
|
||||
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
|
||||
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
|
||||
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
|
||||
B0403CF02AD92D7B00137C09 /* ReleaseNotesView.swift in Sources */,
|
||||
CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */,
|
||||
CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */,
|
||||
E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */,
|
||||
|
|
@ -895,20 +934,25 @@
|
|||
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
|
||||
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
|
||||
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */,
|
||||
B0403CF42AD9381D00137C09 /* SDKsView.swift in Sources */,
|
||||
CAC9F92D25BCDA4400B4965F /* HelperInstallState.swift in Sources */,
|
||||
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */,
|
||||
CAC281CD259F97FA00B8AB0B /* ObservingProgressIndicator.swift in Sources */,
|
||||
CABFA9C22592EEEA00380FEE /* Publisher+Resumable.swift in Sources */,
|
||||
B0C6AD0B2AD9178E00E64698 /* IdenticalBuildView.swift in Sources */,
|
||||
CAFBDC68259A308B003DCC5A /* InfoPane.swift in Sources */,
|
||||
B0403CF82AD991F800137C09 /* UnselectedView.swift in Sources */,
|
||||
E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */,
|
||||
CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */,
|
||||
CAFBDC6C259A3098003DCC5A /* View+Conditional.swift in Sources */,
|
||||
CABFA9CF2592EEEA00380FEE /* Process.swift in Sources */,
|
||||
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */,
|
||||
CABFA9C72592EEEA00380FEE /* Entry+.swift in Sources */,
|
||||
B0403CFA2AD9942A00137C09 /* NotInstalledStateButtons.swift in Sources */,
|
||||
CAE424B4259A764700B8B246 /* AppState+Install.swift in Sources */,
|
||||
CAE42487259A68A300B8B246 /* XcodeListCategory.swift in Sources */,
|
||||
CAA858C425A2BE4E00ACF8C0 /* Downloader.swift in Sources */,
|
||||
B0403CF62AD9849E00137C09 /* CompilersView.swift in Sources */,
|
||||
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */,
|
||||
CA9FF87B2595293E00E47BAF /* DataSource.swift in Sources */,
|
||||
CABFA9C92592EEEA00380FEE /* URLRequest+Apple.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -564,10 +564,10 @@ class AppState: ObservableObject {
|
|||
)
|
||||
}
|
||||
|
||||
func reveal(xcode: Xcode) {
|
||||
func reveal(_ path: Path?) {
|
||||
// TODO: show error if not
|
||||
guard let installedXcodePath = xcode.installedPath else { return }
|
||||
NSWorkspace.shared.activateFileViewerSelecting([installedXcodePath.url])
|
||||
guard let path = path else { return }
|
||||
NSWorkspace.shared.activateFileViewerSelecting([path.url])
|
||||
}
|
||||
|
||||
func reveal(path: String) {
|
||||
|
|
@ -653,8 +653,8 @@ class AppState: ObservableObject {
|
|||
NSPasteboard.general.setString(installedXcodePath.string, forType: .string)
|
||||
}
|
||||
|
||||
func copyReleaseNote(xcode: Xcode) {
|
||||
guard let url = xcode.releaseNotesURL else { return }
|
||||
func copyReleaseNote(from url: URL?) {
|
||||
guard let url = url else { return }
|
||||
NSPasteboard.general.declareTypes([.URL, .string], owner: nil)
|
||||
NSPasteboard.general.writeObjects([url as NSURL])
|
||||
NSPasteboard.general.setString(url.absoluteString, forType: .string)
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ struct RevealButton: View {
|
|||
|
||||
private func reveal() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.reveal(xcode: xcode)
|
||||
appState.reveal(xcode.installedPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,8 +174,9 @@ struct CopyPathButton: View {
|
|||
}
|
||||
|
||||
struct CopyReleaseNoteButton: View {
|
||||
let url: URL?
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
Button(action: copyReleaseNote) {
|
||||
|
|
@ -185,8 +186,8 @@ struct CopyReleaseNoteButton: View {
|
|||
}
|
||||
|
||||
private func copyReleaseNote() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.copyReleaseNote(xcode: xcode)
|
||||
guard let url = url else { return }
|
||||
appState.copyReleaseNote(from: url)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
31
Xcodes/Frontend/InfoPane/CompatibilityView.swift
Normal file
31
Xcodes/Frontend/InfoPane/CompatibilityView.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// CompatibilityView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CompatibilityView: View {
|
||||
let requiredMacOSVersion: String?
|
||||
|
||||
var body: some View {
|
||||
if let requiredMacOSVersion = requiredMacOSVersion {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Compatibility")
|
||||
.font(.headline)
|
||||
Text(String(format: localizeString("MacOSRequirement"), requiredMacOSVersion))
|
||||
.font(.subheadline)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CompatibilityView(requiredMacOSVersion: "10.15.4")
|
||||
.padding()
|
||||
}
|
||||
58
Xcodes/Frontend/InfoPane/CompilersView.swift
Normal file
58
Xcodes/Frontend/InfoPane/CompilersView.swift
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
//
|
||||
// CompilersView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import struct XCModel.Compilers
|
||||
|
||||
struct CompilersView: View {
|
||||
let compilers: Compilers?
|
||||
|
||||
var body: some View {
|
||||
if let compilers = compilers {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Compilers").font(.headline)
|
||||
Text(Self.content(from: compilers)).font(.subheadline)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
static func content(from compilers: Compilers) -> String {
|
||||
[ ("Swift", compilers.swift),
|
||||
("Clang", compilers.clang),
|
||||
("LLVM", compilers.llvm),
|
||||
("LLVM GCC", compilers.llvm_gcc),
|
||||
("GCC", compilers.gcc)
|
||||
].compactMap { // remove nil compiler
|
||||
guard $0.1 != nil, // has version array
|
||||
!$0.1!.isEmpty // has at least 1 version
|
||||
else { return nil }
|
||||
|
||||
let numbers = $0.1!.compactMap { $0.number } // remove nil number
|
||||
guard !numbers.isEmpty // has at least 1 number
|
||||
else { return nil }
|
||||
|
||||
// description for each type of compilers
|
||||
return "\($0.0): \(numbers.joined(separator: ", "))"
|
||||
}.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let 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")
|
||||
)
|
||||
|
||||
return CompilersView(compilers: compilers)
|
||||
.padding()
|
||||
}
|
||||
37
Xcodes/Frontend/InfoPane/IconView.swift
Normal file
37
Xcodes/Frontend/InfoPane/IconView.swift
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// IconView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 11/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Path
|
||||
|
||||
struct IconView: View {
|
||||
let installState: XcodeInstallState
|
||||
|
||||
var body: some View {
|
||||
if case let .installed(path) = installState {
|
||||
Image(nsImage: NSWorkspace.shared.icon(forFile: path.string))
|
||||
} else {
|
||||
Image(systemName: "app.fill")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Installed") {
|
||||
IconView(installState: XcodeInstallState.installed(Path("/Applications/Xcode.app")!))
|
||||
.frame(width: 300, height: 100)
|
||||
.padding()
|
||||
}
|
||||
|
||||
#Preview("Not Installed") {
|
||||
IconView(installState: XcodeInstallState.notInstalled)
|
||||
.frame(width: 300, height: 100)
|
||||
.padding()
|
||||
}
|
||||
62
Xcodes/Frontend/InfoPane/IdenticalBuildView.swift
Normal file
62
Xcodes/Frontend/InfoPane/IdenticalBuildView.swift
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// IdenticalBuildView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 11/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Version
|
||||
|
||||
struct IdenticalBuildsView: View {
|
||||
let builds: [Version]
|
||||
private let isEmpty: Bool
|
||||
private let accessibilityDescription: String
|
||||
|
||||
var body: some View {
|
||||
if isEmpty {
|
||||
EmptyView()
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("IdenticalBuilds")
|
||||
Image(systemName: "square.fill.on.square.fill")
|
||||
.foregroundColor(.secondary)
|
||||
.accessibility(hidden: true)
|
||||
.help("IdenticalBuilds.help")
|
||||
}
|
||||
.font(.headline)
|
||||
|
||||
ForEach(builds, id: \.description) { version in
|
||||
Text("• \(version.appleDescription)")
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
.accessibilityElement()
|
||||
.accessibility(label: Text("IdenticalBuilds"))
|
||||
.accessibility(value: Text(accessibilityDescription))
|
||||
.accessibility(hint: Text("IdenticalBuilds.help"))
|
||||
}
|
||||
}
|
||||
|
||||
init(builds: [Version]) {
|
||||
self.builds = builds
|
||||
self.isEmpty = builds.isEmpty
|
||||
self.accessibilityDescription = builds
|
||||
.map(\.appleDescription)
|
||||
.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
let builds: [Version] = [.init(xcodeVersion: "15.0")!, .init(xcodeVersion: "15.1")!]
|
||||
|
||||
#Preview("Has Some Builds") {
|
||||
IdenticalBuildsView(builds: builds)
|
||||
.padding()
|
||||
}
|
||||
|
||||
#Preview("No Build") {
|
||||
IdenticalBuildsView(builds: [])
|
||||
.padding()
|
||||
}
|
||||
|
|
@ -2,425 +2,134 @@ import AppKit
|
|||
import Path
|
||||
import SwiftUI
|
||||
import Version
|
||||
import struct XCModel.SDKs
|
||||
import struct XCModel.Compilers
|
||||
import struct XCModel.SDKs
|
||||
|
||||
struct InfoPane: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let selectedXcodeID: Xcode.ID?
|
||||
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
|
||||
|
||||
let xcode: Xcode
|
||||
|
||||
var body: some View {
|
||||
if let xcode = appState.allXcodes.first(where: { $0.id == selectedXcodeID }) {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
icon(for: xcode)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
IconView(installState: xcode.installState)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
Text(verbatim: "Xcode \(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)")
|
||||
.font(.title)
|
||||
|
||||
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(xcode: xcode) }) {
|
||||
Image(systemName: "arrow.right.circle.fill")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.help("RevealInFinder")
|
||||
}
|
||||
|
||||
HStack {
|
||||
SelectButton(xcode: xcode)
|
||||
.disabled(xcode.selected)
|
||||
.help("Selected")
|
||||
|
||||
OpenButton(xcode: xcode)
|
||||
.help("Open")
|
||||
|
||||
Spacer()
|
||||
UninstallButton(xcode: xcode)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
Text(verbatim: "Xcode \(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)")
|
||||
.font(.title)
|
||||
|
||||
Group{
|
||||
runtimes(for: xcode)
|
||||
releaseNotes(for: xcode)
|
||||
releaseDate(for: xcode)
|
||||
identicalBuilds(for: xcode)
|
||||
compatibility(for: xcode)
|
||||
sdks(for: xcode)
|
||||
compilers(for: xcode)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(minWidth: 200, maxWidth: .infinity)
|
||||
} else {
|
||||
empty
|
||||
.frame(minWidth: 200, maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func icon(for xcode: Xcode) -> some View {
|
||||
if case let .installed(path) = xcode.installState {
|
||||
Image(nsImage: NSWorkspace.shared.icon(forFile: path.string))
|
||||
} else {
|
||||
Image(systemName: "app.fill")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func identicalBuilds(for xcode: Xcode) -> some View {
|
||||
if !xcode.identicalBuilds.isEmpty {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("IdenticalBuilds")
|
||||
Image(systemName: "square.fill.on.square.fill")
|
||||
.foregroundColor(.secondary)
|
||||
.accessibility(hidden: true)
|
||||
.help("IdenticalBuilds.help")
|
||||
}
|
||||
.font(.headline)
|
||||
|
||||
ForEach(xcode.identicalBuilds, id: \.description) { version in
|
||||
Text("• \(version.appleDescription)")
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.accessibilityElement()
|
||||
.accessibility(label: Text("IdenticalBuilds"))
|
||||
.accessibility(value: Text(xcode.identicalBuilds.map(\.appleDescription).joined(separator: ", ")))
|
||||
.accessibility(hint: Text("IdenticalBuilds.help"))
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
InfoPaneControls(xcode: xcode)
|
||||
|
||||
@ViewBuilder
|
||||
private func releaseDate(for xcode: Xcode) -> some View {
|
||||
if let releaseDate = xcode.releaseDate {
|
||||
VStack(alignment: .leading) {
|
||||
Text("ReleaseDate")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text("\(releaseDate, style: .date)")
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func releaseNotes(for xcode: Xcode) -> some View {
|
||||
if let releaseNotesURL = xcode.releaseNotesURL {
|
||||
Button(action: { openURL(releaseNotesURL) }) {
|
||||
Label("ReleaseNotes", systemImage: "link")
|
||||
}
|
||||
.buttonStyle(LinkButtonStyle())
|
||||
.contextMenu(menuItems: {
|
||||
releaseNotesMenu(for: xcode)
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.help("ReleaseNotes.help")
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
|
||||
@ViewBuilder
|
||||
private func releaseNotesMenu(for xcode: Xcode) -> some View {
|
||||
CopyReleaseNoteButton(xcode: xcode)
|
||||
}
|
||||
|
||||
@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(String(format: localizeString("MacOSRequirement"), requiredMacOSVersion))
|
||||
.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),
|
||||
("visionOS", \.visionOS),
|
||||
], 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)
|
||||
}
|
||||
Group {
|
||||
ReleaseNotesView(url: xcode.releaseNotesURL)
|
||||
ReleaseDateView(date: xcode.releaseDate)
|
||||
IdenticalBuildsView(builds: xcode.identicalBuilds)
|
||||
CompatibilityView(requiredMacOSVersion: xcode.requiredMacOSVersion)
|
||||
SDKsView(sdks: xcode.sdks)
|
||||
CompilersView(compilers: xcode.compilers)
|
||||
}
|
||||
}
|
||||
} 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 func downloadFileSize(for xcode: Xcode) -> some View {
|
||||
// if we've downloaded it no need to show the download size
|
||||
if let downloadFileSize = xcode.downloadFileSizeString, case .notInstalled = xcode.installState {
|
||||
VStack(alignment: .leading) {
|
||||
Text("DownloadSize")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text("\(downloadFileSize)")
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var empty: some View {
|
||||
Text("NoXcodeSelected")
|
||||
.font(.title)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func runtimes(for xcode: Xcode) -> some View {
|
||||
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Platforms")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
let builds = xcode.sdks?.allBuilds()
|
||||
let runtimes = builds?.flatMap { sdkBuild in
|
||||
appState.downloadableRuntimes.filter {
|
||||
$0.sdkBuildUpdate == sdkBuild
|
||||
}
|
||||
}
|
||||
// let runtimes = appState.getRunTimes(xcode: xcode)
|
||||
|
||||
ForEach(runtimes ?? [], id: \.simulatorVersion.buildUpdate) { runtime in
|
||||
VStack {
|
||||
HStack {
|
||||
Text("\(runtime.visibleIdentifier)")
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
Text(runtime.downloadFileSizeString)
|
||||
.font(.subheadline)
|
||||
|
||||
// it's installed if we have a path
|
||||
if let path = appState.runtimeInstallPath(xcode: xcode, runtime: runtime) {
|
||||
Button(action: { appState.reveal(path: path.string) }) {
|
||||
Image(systemName: "arrow.right.circle.fill")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.help("RevealInFinder")
|
||||
} else {
|
||||
DownloadRuntimeButton(runtime: runtime)
|
||||
}
|
||||
}
|
||||
switch runtime.installState {
|
||||
|
||||
case .installing(let installationStep):
|
||||
RuntimeInstallationStepDetailView(installationStep: installationStep)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InfoPane_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
.init(
|
||||
version: Version(major: 12, minor: 3, patch: 0),
|
||||
installState: .installed(Path("/Applications/Xcode-12.3.0.app")!),
|
||||
selected: true,
|
||||
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/")!,
|
||||
releaseDate: Date(),
|
||||
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")
|
||||
),
|
||||
downloadFileSize: 242342424
|
||||
)
|
||||
]
|
||||
})
|
||||
.previewDisplayName("Populated, Installed, Selected")
|
||||
#Preview(PreviewName.allCases[0].rawValue) { makePreviewContent(for: 0) }
|
||||
#Preview(PreviewName.allCases[1].rawValue) { makePreviewContent(for: 1) }
|
||||
#Preview(PreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) }
|
||||
#Preview(PreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) }
|
||||
#Preview(PreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) }
|
||||
|
||||
InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
.init(
|
||||
version: Version(major: 12, minor: 3, patch: 0),
|
||||
installState: .installed(Path("/Applications/Xcode-12.3.0.app")!),
|
||||
selected: false,
|
||||
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")
|
||||
),
|
||||
downloadFileSize: 242342424)
|
||||
]
|
||||
})
|
||||
.previewDisplayName("Populated, Installed, Unselected")
|
||||
private func makePreviewContent(for index: Int) -> some View {
|
||||
let name = PreviewName.allCases[index]
|
||||
|
||||
InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
.init(
|
||||
version: Version(major: 12, minor: 3, patch: 0),
|
||||
installState: .notInstalled,
|
||||
selected: false,
|
||||
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")
|
||||
),
|
||||
downloadFileSize: 242342424)
|
||||
]
|
||||
|
||||
})
|
||||
.previewDisplayName("Populated, Uninstalled")
|
||||
})
|
||||
|
||||
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: .installed(Path("/Applications/Xcode-12.3.0.app")!),
|
||||
selected: false,
|
||||
icon: nil,
|
||||
sdks: nil,
|
||||
compilers: nil)
|
||||
]
|
||||
})
|
||||
.previewDisplayName("Basic, installed")
|
||||
enum PreviewName: String, CaseIterable, Identifiable {
|
||||
case Populated_Installed_Selected
|
||||
case Populated_Installed_Unselected
|
||||
case Populated_Uninstalled
|
||||
case Basic_Installed
|
||||
case Basic_Installing
|
||||
|
||||
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()) {
|
||||
$0.allXcodes = [
|
||||
]
|
||||
})
|
||||
.previewDisplayName("Empty")
|
||||
}
|
||||
.frame(maxWidth: 300)
|
||||
}
|
||||
var id: PreviewName { self }
|
||||
}
|
||||
|
||||
var xcodeDict: [PreviewName: Xcode] = [
|
||||
.Populated_Installed_Selected: .init(
|
||||
version: _versionNoMeta,
|
||||
installState: .installed(Path(_path)!),
|
||||
selected: true,
|
||||
icon: NSWorkspace.shared.icon(forFile: _path),
|
||||
requiredMacOSVersion: _requiredMacOSVersion,
|
||||
releaseNotesURL: URL(string: "https://developer.apple.com/documentation/xcode-release-notes/xcode-12_3-release-notes/")!,
|
||||
releaseDate: Date(),
|
||||
sdks: _sdks,
|
||||
compilers: _compilers,
|
||||
downloadFileSize: _downloadFileSize
|
||||
),
|
||||
.Populated_Installed_Unselected: .init(
|
||||
version: _versionNoMeta,
|
||||
installState: .installed(Path(_path)!),
|
||||
selected: false,
|
||||
icon: NSWorkspace.shared.icon(forFile: _path),
|
||||
sdks: _sdks,
|
||||
compilers: _compilers,
|
||||
downloadFileSize: _downloadFileSize
|
||||
),
|
||||
.Populated_Uninstalled: .init(
|
||||
version: Version(major: 12, minor: 3, patch: 0),
|
||||
installState: .notInstalled,
|
||||
selected: false,
|
||||
icon: nil,
|
||||
sdks: _sdks,
|
||||
compilers: _compilers,
|
||||
downloadFileSize: _downloadFileSize
|
||||
),
|
||||
.Basic_Installed: .init(
|
||||
version: _versionWithMeta,
|
||||
installState: .installed(Path(_path)!),
|
||||
selected: false,
|
||||
icon: nil,
|
||||
sdks: nil,
|
||||
compilers: nil
|
||||
),
|
||||
.Basic_Installing: .init(
|
||||
version: _versionWithMeta,
|
||||
installState: .installing(.downloading(
|
||||
progress: configure(Progress()) {
|
||||
$0.kind = .file
|
||||
$0.fileOperationKind = .downloading
|
||||
$0.estimatedTimeRemaining = 123
|
||||
$0.totalUnitCount = 11_944_848_484
|
||||
$0.completedUnitCount = 848_444_920
|
||||
$0.throughput = 9_211_681
|
||||
}
|
||||
)),
|
||||
selected: false,
|
||||
icon: nil,
|
||||
sdks: nil,
|
||||
compilers: nil
|
||||
),
|
||||
]
|
||||
|
||||
private let _versionNoMeta = Version(major: 12, minor: 3, patch: 0)
|
||||
private let _versionWithMeta = Version(major: 12, minor: 3, patch: 1, buildMetadataIdentifiers: ["1234A"])
|
||||
private let _path = "/Applications/Xcode-12.3.0.app"
|
||||
private let _requiredMacOSVersion = "10.15.4"
|
||||
private let _sdks = SDKs(
|
||||
macOS: .init(number: "11.1"),
|
||||
iOS: .init(number: "14.3"),
|
||||
watchOS: .init(number: "7.3"),
|
||||
tvOS: .init(number: "14.3")
|
||||
)
|
||||
private let _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")
|
||||
)
|
||||
private let _downloadFileSize: Int64 = 242_342_424
|
||||
|
|
|
|||
46
Xcodes/Frontend/InfoPane/InfoPaneControls.swift
Normal file
46
Xcodes/Frontend/InfoPane/InfoPaneControls.swift
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// InfoPaneControls.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 14/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct InfoPaneControls: View {
|
||||
let xcode: Xcode
|
||||
|
||||
var body: some View {
|
||||
VStack (alignment: .leading) {
|
||||
switch xcode.installState {
|
||||
case .notInstalled:
|
||||
NotInstalledStateButtons(
|
||||
downloadFileSizeString: xcode.downloadFileSizeString,
|
||||
id: xcode.id)
|
||||
case .installing(let installationStep):
|
||||
InstallationStepDetailView(installationStep: installationStep)
|
||||
CancelInstallButton(xcode: xcode)
|
||||
case .installed(_):
|
||||
InstalledStateButtons(xcode: xcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(PreviewName.allCases[0].rawValue) { makePreviewContent(for: 0) }
|
||||
#Preview(PreviewName.allCases[1].rawValue) { makePreviewContent(for: 1) }
|
||||
#Preview(PreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) }
|
||||
#Preview(PreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) }
|
||||
#Preview(PreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) }
|
||||
|
||||
private func makePreviewContent(for index: Int) -> some View {
|
||||
let name = PreviewName.allCases[index]
|
||||
|
||||
return InfoPaneControls(xcode: xcodeDict[name]!)
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [xcodeDict[name]!]
|
||||
})
|
||||
.frame(width: 300)
|
||||
.padding()
|
||||
}
|
||||
|
|
@ -25,25 +25,25 @@ struct InstallationStepDetailView: View {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
#Preview("Downloading") {
|
||||
InstallationStepDetailView(
|
||||
installationStep: .downloading(
|
||||
progress: configure(Progress()) {
|
||||
$0.kind = .file
|
||||
$0.fileOperationKind = .downloading
|
||||
$0.estimatedTimeRemaining = 123
|
||||
$0.totalUnitCount = 11944848484
|
||||
$0.completedUnitCount = 848444920
|
||||
$0.throughput = 9211681
|
||||
}
|
||||
)
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
|
||||
#Preview("Unarchiving") {
|
||||
InstallationStepDetailView(
|
||||
installationStep: .unarchiving
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
|
|
|
|||
76
Xcodes/Frontend/InfoPane/InstalledStateButtons.swift
Normal file
76
Xcodes/Frontend/InfoPane/InstalledStateButtons.swift
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// InstallingStateButtons.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Version
|
||||
import XCModel
|
||||
import Path
|
||||
|
||||
struct InstalledStateButtons: View {
|
||||
let xcode: Xcode
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(xcode.installedPath?.string ?? "")
|
||||
Button(action: { appState.reveal(xcode.installedPath) }) {
|
||||
Image(systemName: "arrow.right.circle.fill")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.help("RevealInFinder")
|
||||
}
|
||||
|
||||
HStack {
|
||||
SelectButton(xcode: xcode)
|
||||
.disabled(xcode.selected)
|
||||
.help("Selected")
|
||||
|
||||
OpenButton(xcode: xcode)
|
||||
.help("Open")
|
||||
|
||||
Spacer()
|
||||
UninstallButton(xcode: xcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
InstalledStateButtons(xcode: xcode)
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [xcode]
|
||||
})
|
||||
.padding()
|
||||
.frame(width: 300)
|
||||
}
|
||||
|
||||
private let xcode = Xcode(
|
||||
version: Version(major: 12, minor: 3, patch: 0),
|
||||
installState: .installed(Path("/Applications/Xcode-12.3.0.app")!),
|
||||
selected: true,
|
||||
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/")!,
|
||||
releaseDate: Date(),
|
||||
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")
|
||||
),
|
||||
downloadFileSize: 242342424
|
||||
)
|
||||
44
Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift
Normal file
44
Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// NotInstalledStateButtonsView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Version
|
||||
|
||||
struct NotInstalledStateButtons: View {
|
||||
let downloadFileSizeString: String?
|
||||
let id: Version
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Button {
|
||||
appState.checkMinVersionAndInstall(id: id)
|
||||
} label: {
|
||||
Text("Install") .help("Install")
|
||||
}
|
||||
|
||||
if let size = downloadFileSizeString {
|
||||
Text("DownloadSize")
|
||||
.font(.headline)
|
||||
Text(size)
|
||||
.font(.subheadline)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NotInstalledStateButtons(
|
||||
downloadFileSizeString: "1,19 GB",
|
||||
id: Version(major: 12, minor: 3, patch: 0)
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
35
Xcodes/Frontend/InfoPane/ReleaseDateView.swift
Normal file
35
Xcodes/Frontend/InfoPane/ReleaseDateView.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// ReleaseDateView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 11/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ReleaseDateView: View {
|
||||
let date: Date?
|
||||
|
||||
var body: some View {
|
||||
if let date = date {
|
||||
VStack(alignment: .leading) {
|
||||
Text("ReleaseDate")
|
||||
.font(.headline)
|
||||
Text("\(date, style: .date)")
|
||||
.font(.subheadline)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
init(date: Date? = nil) {
|
||||
self.date = date
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ReleaseDateView(date: Date())
|
||||
.padding()
|
||||
}
|
||||
38
Xcodes/Frontend/InfoPane/ReleaseNotesView.swift
Normal file
38
Xcodes/Frontend/InfoPane/ReleaseNotesView.swift
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// ReleaseNotesView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ReleaseNotesView: View {
|
||||
let url: URL?
|
||||
|
||||
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
|
||||
|
||||
var body: some View {
|
||||
if let url = url {
|
||||
Button(action: { openURL(url) }) {
|
||||
Label("ReleaseNotes", systemImage: "link")
|
||||
}
|
||||
.buttonStyle(LinkButtonStyle())
|
||||
.contextMenu(menuItems: {
|
||||
CopyReleaseNoteButton(url: url)
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.help("ReleaseNotes.help")
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let url = URL(string: "https://developer.apple.com/documentation/xcode-release-notes/xcode-12_3-release-notes/")!
|
||||
|
||||
return ReleaseNotesView(url: url)
|
||||
.padding()
|
||||
}
|
||||
68
Xcodes/Frontend/InfoPane/SDKsView.swift
Normal file
68
Xcodes/Frontend/InfoPane/SDKsView.swift
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// SDKsView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import struct XCModel.SDKs
|
||||
|
||||
struct SDKsView: View {
|
||||
let content: String
|
||||
|
||||
var body: some View {
|
||||
if content.isEmpty {
|
||||
EmptyView()
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
Text("SDKs").font(.headline)
|
||||
Text(content).font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(sdks: SDKs?) {
|
||||
guard let sdks = sdks else {
|
||||
self.content = ""
|
||||
return
|
||||
}
|
||||
let content = Self.content(from: sdks)
|
||||
self.content = content
|
||||
}
|
||||
|
||||
static private func content(from sdks: SDKs) -> String {
|
||||
let content: String = [
|
||||
("macOS", sdks.macOS),
|
||||
("iOS", sdks.iOS),
|
||||
("watchOS", sdks.watchOS),
|
||||
("tvOS", sdks.tvOS)
|
||||
].compactMap { // remove nil compiler
|
||||
guard $0.1 != nil, // has version array
|
||||
!$0.1!.isEmpty // has at least 1 version
|
||||
else { return nil }
|
||||
|
||||
let numbers = $0.1!.compactMap { $0.number } // remove nil number
|
||||
guard !numbers.isEmpty // has at least 1 number
|
||||
else { return nil }
|
||||
|
||||
// description for each type of compilers
|
||||
return "\($0.0): \(numbers.joined(separator: ", "))"
|
||||
}.joined(separator: "\n")
|
||||
.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let sdks = SDKs(
|
||||
macOS: .init(number: "11.1"),
|
||||
iOS: .init(number: "14.3"),
|
||||
watchOS: .init(number: "7.3"),
|
||||
tvOS: .init(number: "14.3"))
|
||||
|
||||
return SDKsView(sdks: sdks)
|
||||
.padding()
|
||||
}
|
||||
26
Xcodes/Frontend/InfoPane/UnselectedView.swift
Normal file
26
Xcodes/Frontend/InfoPane/UnselectedView.swift
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// UnselectedView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UnselectedView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text("NoXcodeSelected")
|
||||
.font(.title)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
UnselectedView()
|
||||
.padding()
|
||||
}
|
||||
|
|
@ -27,8 +27,15 @@ struct MainWindow: View {
|
|||
}
|
||||
|
||||
if isShowingInfoPane {
|
||||
InfoPane(selectedXcodeID: selectedXcodeID)
|
||||
.frame(minWidth: 300, maxWidth: .infinity)
|
||||
Group {
|
||||
if let xcode = xcode {
|
||||
InfoPane(xcode: xcode)
|
||||
} else {
|
||||
UnselectedView()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(minWidth: 300, maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.mainToolbar(
|
||||
|
|
@ -59,7 +66,11 @@ struct MainWindow: View {
|
|||
// FB8954571 focusedValue(_:_:) on List row doesn't propagate value to @FocusedValue
|
||||
.focusedValue(\.selectedXcode, SelectedXcode(appState.allXcodes.first { $0.id == selectedXcodeID }))
|
||||
}
|
||||
|
||||
|
||||
private var xcode: Xcode? {
|
||||
appState.allXcodes.first(where: { $0.id == selectedXcodeID })
|
||||
}
|
||||
|
||||
private var subtitleText: Text {
|
||||
if let lastUpdated = lastUpdated.map(Date.init(timeIntervalSince1970:)) {
|
||||
return Text("\(localizeString("UpdatedAt")) \(lastUpdated, style: .date) \(lastUpdated, style: .time)")
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ struct XcodesApp: App {
|
|||
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
|
||||
@StateObject private var appState = AppState()
|
||||
@StateObject private var updater = ObservableUpdater()
|
||||
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup("Xcodes") {
|
||||
MainWindow()
|
||||
.environmentObject(appState)
|
||||
.environmentObject(updater)
|
||||
// This is intentionally used on a View, and not on a WindowGroup,
|
||||
// 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.
|
||||
// When used on a View it's also invoked on launch, which doesn't occur with a WindowGroup.
|
||||
// When used on a View it's also invoked on launch, which doesn't occur with a WindowGroup.
|
||||
// FB8954581 ScenePhase read from App doesn't return a value on launch
|
||||
.onChange(of: scenePhase) { newScenePhase in
|
||||
guard !isTesting else { return }
|
||||
|
|
@ -37,7 +37,7 @@ struct XcodesApp: App {
|
|||
updater.checkForUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
CommandGroup(after: CommandGroupPlacement.newItem) {
|
||||
Button("Refresh") {
|
||||
appState.update()
|
||||
|
|
@ -47,33 +47,33 @@ struct XcodesApp: App {
|
|||
}
|
||||
|
||||
XcodeCommands(appState: appState)
|
||||
|
||||
|
||||
CommandGroup(replacing: CommandGroupPlacement.help) {
|
||||
Button("Menu.GitHubRepo") {
|
||||
let xcodesRepoURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/")!
|
||||
openURL(xcodesRepoURL)
|
||||
}
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
Button("Menu.ReportABug") {
|
||||
let bugReportURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/issues/new?assignees=&labels=bug&template=bug_report.md&title=")!
|
||||
openURL(bugReportURL)
|
||||
}
|
||||
|
||||
|
||||
Button("Menu.RequestNewFeature") {
|
||||
let featureRequestURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=")!
|
||||
openURL(featureRequestURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
Settings {
|
||||
PreferencesView()
|
||||
.environmentObject(appState)
|
||||
.environmentObject(updater)
|
||||
}
|
||||
#endif
|
||||
#if os(macOS)
|
||||
Settings {
|
||||
PreferencesView()
|
||||
.environmentObject(appState)
|
||||
.environmentObject(updater)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
$0.contentView = NSHostingView(rootView: AboutView(showAcknowledgementsWindow: showAcknowledgementsWindow))
|
||||
$0.isReleasedWhenClosed = false
|
||||
}
|
||||
|
||||
|
||||
private let acknowledgementsWindow = configure(NSWindow(
|
||||
contentRect: .zero,
|
||||
styleMask: [.closable, .resizable, .miniaturizable, .titled],
|
||||
|
|
@ -103,21 +103,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
/// If we wanted to use only SwiftUI API to do this we could make a new WindowGroup and use openURL and handlesExternalEvents.
|
||||
/// WindowGroup lets the user open more than one window right now, which is a little strange for an About window.
|
||||
/// (It's also weird that the main Xcode list window can be opened more than once, there should only be one.)
|
||||
/// To work around this, an AppDelegate holds onto a single instance of an NSWindow that is shown here.
|
||||
/// To work around this, an AppDelegate holds onto a single instance of an NSWindow that is shown here.
|
||||
/// FB8954588 Scene / WindowGroup is missing API to limit the number of windows that can be created
|
||||
func showAboutWindow() {
|
||||
aboutWindow.center()
|
||||
aboutWindow.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
|
||||
func showAcknowledgementsWindow() {
|
||||
acknowledgementsWindow.center()
|
||||
acknowledgementsWindow.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_: Notification) {}
|
||||
}
|
||||
|
||||
func localizeString(_ key: String, comment: String = "") -> String {
|
||||
|
|
@ -126,5 +124,4 @@ func localizeString(_ key: String, comment: String = "") -> String {
|
|||
} else {
|
||||
return NSLocalizedString(key, comment: comment)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue