diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 0ea5669..4ce7a31 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -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 */; }; @@ -192,6 +203,17 @@ A0187D39285792D1002F46F9 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; AAB037D32839BD4700017680 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; AB4EB0DE28541FA000FF3B1D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + B0403CEF2AD92D7B00137C09 /* ReleaseNotesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseNotesView.swift; sourceTree = ""; }; + B0403CF12AD934B600137C09 /* CompatibilityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibilityView.swift; sourceTree = ""; }; + B0403CF32AD9381D00137C09 /* SDKsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKsView.swift; sourceTree = ""; }; + B0403CF52AD9849E00137C09 /* CompilersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompilersView.swift; sourceTree = ""; }; + B0403CF72AD991F800137C09 /* UnselectedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnselectedView.swift; sourceTree = ""; }; + B0403CF92AD9942A00137C09 /* NotInstalledStateButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotInstalledStateButtons.swift; sourceTree = ""; }; + B0403CFB2AD9A6BF00137C09 /* InstalledStateButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledStateButtons.swift; sourceTree = ""; }; + B0403CFD2ADA712C00137C09 /* InfoPaneControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPaneControls.swift; sourceTree = ""; }; + B0C6AD032AD6E65700E64698 /* ReleaseDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseDateView.swift; sourceTree = ""; }; + B0C6AD0A2AD9178E00E64698 /* IdenticalBuildView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdenticalBuildView.swift; sourceTree = ""; }; + B0C6AD0C2AD91D7900E64698 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = ""; }; B648F22B2810C1130096781B /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; C0AE7FA4283002DC00DA63D2 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = ""; }; @@ -615,8 +637,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 */, ); path = InfoPane; sourceTree = ""; @@ -841,7 +874,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 */, @@ -859,13 +894,17 @@ 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 */, CA42DD6E25AEA8B200BC0B0C /* Logger.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 */, CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */, @@ -883,20 +922,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 */, diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 49e22de..168369f 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -541,10 +541,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) { @@ -630,8 +630,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) diff --git a/Xcodes/Backend/XcodeCommands.swift b/Xcodes/Backend/XcodeCommands.swift index 76e9924..6767afe 100644 --- a/Xcodes/Backend/XcodeCommands.swift +++ b/Xcodes/Backend/XcodeCommands.swift @@ -151,7 +151,7 @@ struct RevealButton: View { private func reveal() { guard let xcode = xcode else { return } - appState.reveal(xcode: xcode) + appState.reveal(xcode.installedPath) } } @@ -173,8 +173,9 @@ struct CopyPathButton: View { } struct CopyReleaseNoteButton: View { + let url: URL? + @EnvironmentObject var appState: AppState - let xcode: Xcode? var body: some View { Button(action: copyReleaseNote) { @@ -184,8 +185,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) } } diff --git a/Xcodes/Frontend/InfoPane/CompatibilityView.swift b/Xcodes/Frontend/InfoPane/CompatibilityView.swift new file mode 100644 index 0000000..2cf3b40 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/CompatibilityView.swift @@ -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() +} diff --git a/Xcodes/Frontend/InfoPane/CompilersView.swift b/Xcodes/Frontend/InfoPane/CompilersView.swift new file mode 100644 index 0000000..b08458a --- /dev/null +++ b/Xcodes/Frontend/InfoPane/CompilersView.swift @@ -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() +} diff --git a/Xcodes/Frontend/InfoPane/IconView.swift b/Xcodes/Frontend/InfoPane/IconView.swift new file mode 100644 index 0000000..c8bf32f --- /dev/null +++ b/Xcodes/Frontend/InfoPane/IconView.swift @@ -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() +} diff --git a/Xcodes/Frontend/InfoPane/IdenticalBuildView.swift b/Xcodes/Frontend/InfoPane/IdenticalBuildView.swift new file mode 100644 index 0000000..2eac1f0 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/IdenticalBuildView.swift @@ -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() +} diff --git a/Xcodes/Frontend/InfoPane/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift index 3842a55..e1215c8 100644 --- a/Xcodes/Frontend/InfoPane/InfoPane.swift +++ b/Xcodes/Frontend/InfoPane/InfoPane.swift @@ -2,372 +2,140 @@ 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{ - 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) } + + Spacer() } - } 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() - } } -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") - - 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) - } + return InfoPane(xcode: xcodeDict[name]!) + .environmentObject(configure(AppState()) { + $0.allXcodes = [xcodeDict[name]!] + }) + .frame(width: 300, height: 400) + .padding() } + +enum PreviewName: String, CaseIterable, Identifiable { + case Populated_Installed_Selected + case Populated_Installed_Unselected + case Populated_Uninstalled + case Basic_Installed + case Basic_Installing + + 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 diff --git a/Xcodes/Frontend/InfoPane/InfoPaneControls.swift b/Xcodes/Frontend/InfoPane/InfoPaneControls.swift new file mode 100644 index 0000000..6034a38 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/InfoPaneControls.swift @@ -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() +} diff --git a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift index 863f204..f9fb7d8 100644 --- a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift +++ b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift @@ -24,25 +24,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() } diff --git a/Xcodes/Frontend/InfoPane/InstalledStateButtons.swift b/Xcodes/Frontend/InfoPane/InstalledStateButtons.swift new file mode 100644 index 0000000..3c60224 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/InstalledStateButtons.swift @@ -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 +) diff --git a/Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift b/Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift new file mode 100644 index 0000000..28a187e --- /dev/null +++ b/Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift @@ -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() +} diff --git a/Xcodes/Frontend/InfoPane/ReleaseDateView.swift b/Xcodes/Frontend/InfoPane/ReleaseDateView.swift new file mode 100644 index 0000000..c7ee304 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/ReleaseDateView.swift @@ -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() +} diff --git a/Xcodes/Frontend/InfoPane/ReleaseNotesView.swift b/Xcodes/Frontend/InfoPane/ReleaseNotesView.swift new file mode 100644 index 0000000..10e3638 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/ReleaseNotesView.swift @@ -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() +} diff --git a/Xcodes/Frontend/InfoPane/SDKsView.swift b/Xcodes/Frontend/InfoPane/SDKsView.swift new file mode 100644 index 0000000..0d3552e --- /dev/null +++ b/Xcodes/Frontend/InfoPane/SDKsView.swift @@ -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() +} diff --git a/Xcodes/Frontend/InfoPane/UnselectedView.swift b/Xcodes/Frontend/InfoPane/UnselectedView.swift new file mode 100644 index 0000000..f84cb06 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/UnselectedView.swift @@ -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() +} diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift index fcde005..5c74ea4 100644 --- a/Xcodes/Frontend/MainWindow.swift +++ b/Xcodes/Frontend/MainWindow.swift @@ -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)") diff --git a/Xcodes/XcodesApp.swift b/Xcodes/XcodesApp.swift index 76fb1a3..e735bb8 100644 --- a/Xcodes/XcodesApp.swift +++ b/Xcodes/XcodesApp.swift @@ -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) } - }