diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index a1ee749..6ace20e 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -115,7 +115,9 @@ E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.swift */; }; E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */; }; E84B7D0D2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */; }; + E84B7D0F2B30986700DBDA2B /* InfoPane2.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84B7D0E2B30986700DBDA2B /* InfoPane2.swift */; }; E84CF8C12B0FEB8300ECA259 /* RuntimesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84CF8C02B0FEB8300ECA259 /* RuntimesView.swift */; }; + E86671272B309D2F0048559A /* PlatformsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86671262B309D2F0048559A /* PlatformsView.swift */; }; E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; }; E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; }; E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; }; @@ -311,8 +313,10 @@ E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = ""; }; E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeInstallationStepDetailView.swift; sourceTree = ""; }; E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitViewWrapper.swift; sourceTree = ""; }; + E84B7D0E2B30986700DBDA2B /* InfoPane2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPane2.swift; sourceTree = ""; }; E84CF8C02B0FEB8300ECA259 /* RuntimesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimesView.swift; sourceTree = ""; }; E856BB73291EDD3D00DC438B /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = ""; }; + E86671262B309D2F0048559A /* PlatformsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformsView.swift; sourceTree = ""; }; E87AB3C42939B65E00D72F43 /* Hardware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hardware.swift; sourceTree = ""; }; E87DD6EA25D053FA00D86808 /* Progress+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+.swift"; sourceTree = ""; }; E89342F925EDCC17007CF557 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; @@ -648,6 +652,8 @@ B0C6AD0C2AD91D7900E64698 /* IconView.swift */, E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */, E84CF8C02B0FEB8300ECA259 /* RuntimesView.swift */, + E84B7D0E2B30986700DBDA2B /* InfoPane2.swift */, + E86671262B309D2F0048559A /* PlatformsView.swift */, ); path = InfoPane; sourceTree = ""; @@ -911,6 +917,7 @@ CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */, E8D655C0288DD04700A139C2 /* SelectedActionType.swift in Sources */, 36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */, + E86671272B309D2F0048559A /* PlatformsView.swift in Sources */, CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */, CABFA9CE2592EEEA00380FEE /* Version+Xcode.swift in Sources */, CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */, @@ -922,6 +929,7 @@ E8B20CBF2A2EDEC20057D816 /* SDKs+Xcode.swift in Sources */, CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */, CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */, + E84B7D0F2B30986700DBDA2B /* InfoPane2.swift in Sources */, E84B7D0D2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift in Sources */, CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */, CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */, diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index f39c105..9143667 100644 --- a/Xcodes/Backend/AppState+Runtimes.swift +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -36,6 +36,7 @@ extension AppState { Task { do { let runtimes = try await self.runtimeService.localInstalledRuntimes() + DispatchQueue.main.async { self.installedRuntimes = runtimes } diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 7de4af7..998e9f8 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -163,7 +163,6 @@ class AppState: ObservableObject { checkIfHelperIsInstalled() setupAutoInstallTimer() setupDefaults() - updateInstalledRuntimes() } func setupDefaults() { @@ -410,10 +409,7 @@ class AppState: ObservableObject { // Check to see if users MacOS is supported if let requiredMacOSVersion = availableXcode.requiredMacOSVersion { - let split = requiredMacOSVersion.components(separatedBy: ".").compactMap { Int($0) } - let xcodeMinimumMacOSVersion = OperatingSystemVersion(majorVersion: split[safe: 0] ?? 0, minorVersion: split[safe: 1] ?? 0, patchVersion: split[safe: 2] ?? 0) - - if !ProcessInfo.processInfo.isOperatingSystemAtLeast(xcodeMinimumMacOSVersion) { + if hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) { // prompt self.presentedAlert = .checkMinSupportedVersion(xcode: availableXcode, macOS: ProcessInfo.processInfo.operatingSystemVersion.versionString()) return @@ -428,6 +424,13 @@ class AppState: ObservableObject { } } + func hasMinSupportedOS(requiredMacOSVersion: String) -> Bool { + let split = requiredMacOSVersion.components(separatedBy: ".").compactMap { Int($0) } + let xcodeMinimumMacOSVersion = OperatingSystemVersion(majorVersion: split[safe: 0] ?? 0, minorVersion: split[safe: 1] ?? 0, patchVersion: split[safe: 2] ?? 0) + + return !ProcessInfo.processInfo.isOperatingSystemAtLeast(xcodeMinimumMacOSVersion) + } + func install(id: Xcode.ID) { guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return } diff --git a/Xcodes/Backend/SDKs+Xcode.swift b/Xcodes/Backend/SDKs+Xcode.swift index 1462b96..e01f3b4 100644 --- a/Xcodes/Backend/SDKs+Xcode.swift +++ b/Xcodes/Backend/SDKs+Xcode.swift @@ -8,6 +8,8 @@ import Foundation import struct XCModel.SDKs +import XcodesKit +import SwiftUI extension SDKs { /// Loops through all SDK's and returns an array of buildNumbers (to be used to correlate runtimes) @@ -33,3 +35,24 @@ extension SDKs { return buildNumbers } } + +extension DownloadableRuntime { + func icon() -> Image { + switch self.platform { + case .iOS: + return Image(systemName: "iphone") + case .macOS: + return Image(systemName: "macwindow") + case .watchOS: + return Image(systemName: "applewatch") + case .tvOS: + return Image(systemName: "appletv") + case .visionOS: + if #available(macOS 14, *) { + return Image(systemName: "visionpro") + } else { + return Image(systemName: "eyeglasses") + } + } + } +} diff --git a/Xcodes/Backend/XcodeCommands.swift b/Xcodes/Backend/XcodeCommands.swift index 97f36b3..b168bd4 100644 --- a/Xcodes/Backend/XcodeCommands.swift +++ b/Xcodes/Backend/XcodeCommands.swift @@ -61,9 +61,10 @@ struct CancelInstallButton: View { var body: some View { Button(action: cancelInstall) { - Text("Cancel") - .help(localizeString("StopInstallation")) + Image(systemName: "xmark.circle.fill") } + .help(localizeString("StopInstallation")) + .buttonStyle(.plain) } private func cancelInstall() { @@ -78,9 +79,9 @@ struct CancelRuntimeInstallButton: View { var body: some View { Button(action: cancelInstall) { - Text("Cancel") - .help(localizeString("StopInstallation")) - } + Image(systemName: "xmark.circle.fill") + }.help(localizeString("StopInstallation")) + .buttonStyle(.plain) } private func cancelInstall() { diff --git a/Xcodes/Frontend/InfoPane/CompatibilityView.swift b/Xcodes/Frontend/InfoPane/CompatibilityView.swift index 2cf3b40..a81183b 100644 --- a/Xcodes/Frontend/InfoPane/CompatibilityView.swift +++ b/Xcodes/Frontend/InfoPane/CompatibilityView.swift @@ -9,16 +9,30 @@ import SwiftUI struct CompatibilityView: View { + @EnvironmentObject var appState: AppState + 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) + HStack(alignment: .top){ + VStack(alignment: .leading) { + Text("Compatibility") + .font(.headline) + Text(String(format: localizeString("MacOSRequirement"), requiredMacOSVersion)) + .font(.subheadline) + .foregroundColor(appState.hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) ? .red : .primary) + } + Spacer() + if appState.hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.largeTitle) + } } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(.background) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) } else { EmptyView() } @@ -28,4 +42,5 @@ struct CompatibilityView: View { #Preview { CompatibilityView(requiredMacOSVersion: "10.15.4") .padding() + .environmentObject(AppState()) } diff --git a/Xcodes/Frontend/InfoPane/IdenticalBuildView.swift b/Xcodes/Frontend/InfoPane/IdenticalBuildView.swift index 2eac1f0..c09f20a 100644 --- a/Xcodes/Frontend/InfoPane/IdenticalBuildView.swift +++ b/Xcodes/Frontend/InfoPane/IdenticalBuildView.swift @@ -33,6 +33,10 @@ struct IdenticalBuildsView: View { .font(.subheadline) } } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(.background) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) .accessibilityElement() .accessibility(label: Text("IdenticalBuilds")) .accessibility(value: Text(accessibilityDescription)) diff --git a/Xcodes/Frontend/InfoPane/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift index d39648a..624e89f 100644 --- a/Xcodes/Frontend/InfoPane/InfoPane.swift +++ b/Xcodes/Frontend/InfoPane/InfoPane.swift @@ -1,4 +1,5 @@ import AppKit +import XcodesKit import Path import SwiftUI import Version @@ -23,7 +24,7 @@ struct InfoPane: View { Group { RuntimesView(xcode: xcode) - ReleaseDateView(date: xcode.releaseDate) + ReleaseDateView(date: xcode.releaseDate, url: xcode.releaseNotesURL) ReleaseNotesView(url: xcode.releaseNotesURL) IdenticalBuildsView(builds: xcode.identicalBuilds) CompatibilityView(requiredMacOSVersion: xcode.requiredMacOSVersion) @@ -37,14 +38,14 @@ struct InfoPane: View { } } -#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) } +#Preview(XcodePreviewName.allCases[0].rawValue) { makePreviewContent(for: 0) } +#Preview(XcodePreviewName.allCases[1].rawValue) { makePreviewContent(for: 1) } +#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) } +#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) } +#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) } private func makePreviewContent(for index: Int) -> some View { - let name = PreviewName.allCases[index] + let name = XcodePreviewName.allCases[index] return InfoPane(xcode: xcodeDict[name]!) .environmentObject(configure(AppState()) { $0.allXcodes = [xcodeDict[name]!] @@ -53,17 +54,17 @@ private func makePreviewContent(for index: Int) -> some View { .padding() } -enum PreviewName: String, CaseIterable, Identifiable { +enum XcodePreviewName: 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 id: XcodePreviewName { self } } -var xcodeDict: [PreviewName: Xcode] = [ +var xcodeDict: [XcodePreviewName: Xcode] = [ .Populated_Installed_Selected: .init( version: _versionNoMeta, installState: .installed(Path(_path)!), @@ -121,15 +122,48 @@ var xcodeDict: [PreviewName: Xcode] = [ ), ] +var downloadableRuntimes: [DownloadableRuntime] = { + var runtimes = try! JSONDecoder().decode([DownloadableRuntime].self, from: Current.files.contents(atPath: Path.runtimeCacheFile.string)!) + // set iOS to installed + let iOSIndex = runtimes.firstIndex { $0.sdkBuildUpdate == "19E239" }! + var iOSRuntime = runtimes[iOSIndex] + iOSRuntime.installState = .installed + runtimes[iOSIndex] = iOSRuntime + + let watchOSIndex = runtimes.firstIndex { $0.sdkBuildUpdate == "20R362" }! + var runtime = runtimes[watchOSIndex] + runtime.installState = .installing( + RuntimeInstallationStep.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 + } + ) + ) + runtimes[watchOSIndex] = runtime + + return runtimes +}() + +var installedRuntimes: [CoreSimulatorImage] = { + [CoreSimulatorImage(uuid: "85B22F5B-048B-4331-B6E2-F4196D8B7475", path: ["relative" : "file:///Library/Developer/CoreSimulator/Images/85B22F5B-048B-4331-B6E2-F4196D8B7475.dmg"], runtimeInfo: CoreSimulatorRuntimeInfo(build: "19E240"))] // same as iOS in _SDK's +}() + + 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") + iOS: .init(number: "15.4", "19E239"), + watchOS: .init(number: "7.3", "20R362"), + tvOS: .init(number: "14.3", "20K67"), + visionOS: .init(number: "1.0", "21N5233e") ) private let _compilers = Compilers( gcc: .init(number: "4"), diff --git a/Xcodes/Frontend/InfoPane/InfoPane2.swift b/Xcodes/Frontend/InfoPane/InfoPane2.swift new file mode 100644 index 0000000..8f8eaa8 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/InfoPane2.swift @@ -0,0 +1,176 @@ +// +// InfoPane2.swift +// Xcodes +// +// Created by Matt Kiazyk on 2023-12-18. +// + +import AppKit +import Path +import SwiftUI +import Version +import struct XCModel.Compilers +import struct XCModel.SDKs + +struct InfoPane2: View { + + let xcode: Xcode + var body: some View { + ScrollView(.vertical) { + HStack(alignment: .top) { + VStack { + VStack(spacing: 5) { + HStack { + IconView(installState: xcode.installState) + + Text(verbatim: "Xcode \(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)") + .font(.title) + .frame(maxWidth: .infinity, alignment: .leading) + } + InfoPaneControls(xcode: xcode) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(.background) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) + + + VStack { + Text("Platforms") + .font(.title3) + .frame(maxWidth: .infinity, alignment: .leading) + PlatformsView(xcode: xcode) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(.background) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) + } + + VStack(alignment: .leading) { + ReleaseDateView(date: xcode.releaseDate, url: xcode.releaseNotesURL) + CompatibilityView(requiredMacOSVersion: xcode.requiredMacOSVersion) + IdenticalBuildsView(builds: xcode.identicalBuilds) + SDKandCompilers + } + .frame(width: 200) + + } + } + } + + @ViewBuilder + var SDKandCompilers: some View { + VStack(alignment: .leading, spacing: 16) { + SDKsView(sdks: xcode.sdks) + CompilersView(compilers: xcode.compilers) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(.background) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) + } +} + +#Preview(XcodePreviewName.allCases[0].rawValue) { makePreviewContent(for: 0) } +#Preview(XcodePreviewName.allCases[1].rawValue) { makePreviewContent(for: 1) } +#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) } +#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) } +#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) } + +private func makePreviewContent(for index: Int) -> some View { + let name = XcodePreviewName.allCases[index] + return InfoPane2(xcode: xcodeDict[name]!) + .environmentObject(configure(AppState()) { + $0.allXcodes = [xcodeDict[name]!] + }) + .frame(width: 600, height: 400) + .padding() +} + +enum Preview2Name: String, CaseIterable, Identifiable { + case Populated_Installed_Selected + case Populated_Installed_Unselected + case Populated_Uninstalled + case Basic_Installed + case Basic_Installing + + var id: Preview2Name { self } +} + +var xcodeDict2: [Preview2Name: 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 index 6034a38..73cdb35 100644 --- a/Xcodes/Frontend/InfoPane/InfoPaneControls.swift +++ b/Xcodes/Frontend/InfoPane/InfoPaneControls.swift @@ -15,12 +15,18 @@ struct InfoPaneControls: View { VStack (alignment: .leading) { switch xcode.installState { case .notInstalled: - NotInstalledStateButtons( - downloadFileSizeString: xcode.downloadFileSizeString, - id: xcode.id) + HStack { + Spacer() + NotInstalledStateButtons( + downloadFileSizeString: xcode.downloadFileSizeString, + id: xcode.id) + } + case .installing(let installationStep): - InstallationStepDetailView(installationStep: installationStep) - CancelInstallButton(xcode: xcode) + HStack(alignment: .top) { + InstallationStepDetailView(installationStep: installationStep) + CancelInstallButton(xcode: xcode) + } case .installed(_): InstalledStateButtons(xcode: xcode) } @@ -28,14 +34,14 @@ struct InfoPaneControls: View { } } -#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) } +#Preview(XcodePreviewName.allCases[0].rawValue) { makePreviewContent(for: 0) } +#Preview(XcodePreviewName.allCases[1].rawValue) { makePreviewContent(for: 1) } +#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) } +#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) } +#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) } private func makePreviewContent(for index: Int) -> some View { - let name = PreviewName.allCases[index] + let name = XcodePreviewName.allCases[index] return InfoPaneControls(xcode: xcodeDict[name]!) .environmentObject(configure(AppState()) { diff --git a/Xcodes/Frontend/InfoPane/PlatformsView.swift b/Xcodes/Frontend/InfoPane/PlatformsView.swift new file mode 100644 index 0000000..184e597 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/PlatformsView.swift @@ -0,0 +1,104 @@ +// +// PlatformsView.swift +// Xcodes +// +// Created by Matt Kiazyk on 2023-12-18. +// + +import Foundation +import SwiftUI +import XcodesKit + +struct PlatformsView: View { + @EnvironmentObject var appState: AppState + + let xcode: Xcode + + var body: some View { + + let builds = xcode.sdks?.allBuilds() + let runtimes = builds?.flatMap { sdkBuild in + appState.downloadableRuntimes.filter { + $0.sdkBuildUpdate == sdkBuild + } + } + + ForEach(runtimes ?? [], id: \.simulatorVersion.buildUpdate) { runtime in + runtimeView(runtime: runtime) + .frame(minWidth: 200) + .padding() + .background(.quinary) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) + } + } + + @ViewBuilder + func runtimeView(runtime: DownloadableRuntime) -> some View { + VStack(spacing: 10) { + HStack { + runtime.icon() + Text("\(runtime.visibleIdentifier)") + .font(.headline) + pathIfAvailable(xcode: xcode, runtime: runtime) + Spacer() + Text(runtime.downloadFileSizeString) + .font(.subheadline) + } + switch runtime.installState { + case .installed: + EmptyView() + case .notInstalled: + // TODO: Update the downloadableRuntimes with the appropriate installState so we don't have to check path awkwardly + if let path = appState.runtimeInstallPath(xcode: xcode, runtime: runtime) { + EmptyView() + } else { + HStack { + Spacer() + DownloadRuntimeButton(runtime: runtime) + } + } + + case .installing(let installationStep): + HStack(alignment: .top, spacing: 5){ + RuntimeInstallationStepDetailView(installationStep: installationStep) + .fixedSize(horizontal: false, vertical: true) + CancelRuntimeInstallButton(runtime: runtime) + } + + } + } + } + + @ViewBuilder + func pathIfAvailable(xcode: Xcode, runtime: DownloadableRuntime) -> some View { + 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 { + EmptyView() + } + } +} + +#Preview(XcodePreviewName.allCases[0].rawValue) { makePreviewContent(for: 0) } + +private func makePreviewContent(for index: Int) -> some View { + let name = XcodePreviewName.allCases[index] + let runtimes = downloadableRuntimes + + return PlatformsView(xcode: xcodeDict[name]!) + .environmentObject({ () -> AppState in + let a = AppState() + a.allXcodes = [xcodeDict[name]!] + a.installedRuntimes = installedRuntimes + a.downloadableRuntimes = runtimes + + return a + + }()) + .frame(width: 300) + .padding() +} diff --git a/Xcodes/Frontend/InfoPane/ReleaseDateView.swift b/Xcodes/Frontend/InfoPane/ReleaseDateView.swift index c7ee304..838c365 100644 --- a/Xcodes/Frontend/InfoPane/ReleaseDateView.swift +++ b/Xcodes/Frontend/InfoPane/ReleaseDateView.swift @@ -10,26 +10,37 @@ import SwiftUI struct ReleaseDateView: View { let date: Date? - + let url: URL? var body: some View { if let date = date { - VStack(alignment: .leading) { - Text("ReleaseDate") - .font(.headline) - Text("\(date, style: .date)") - .font(.subheadline) - } + + VStack(alignment: .leading) { + HStack { + Text("ReleaseDate") + .font(.headline) + Spacer() + if let url { + ReleaseNotesView(url: url) + } + } + + Text("\(date, style: .date)") + .font(.subheadline) + + } + + + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(.background) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) } else { EmptyView() } } - - init(date: Date? = nil) { - self.date = date - } } #Preview { - ReleaseDateView(date: Date()) + ReleaseDateView(date: Date(), url: URL(string: "https://www.xcodes.app")!) .padding() } diff --git a/Xcodes/Frontend/InfoPane/ReleaseNotesView.swift b/Xcodes/Frontend/InfoPane/ReleaseNotesView.swift index 10e3638..8f16e3a 100644 --- a/Xcodes/Frontend/InfoPane/ReleaseNotesView.swift +++ b/Xcodes/Frontend/InfoPane/ReleaseNotesView.swift @@ -16,13 +16,13 @@ struct ReleaseNotesView: View { var body: some View { if let url = url { Button(action: { openURL(url) }) { - Label("ReleaseNotes", systemImage: "link") + Image(systemName: "link.circle.fill") + .font(.title) } - .buttonStyle(LinkButtonStyle()) + .buttonStyle(.plain) .contextMenu(menuItems: { CopyReleaseNoteButton(url: url) }) - .frame(maxWidth: .infinity, alignment: .leading) .help("ReleaseNotes.help") } else { EmptyView() diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift index 406d2a6..f0a75e4 100644 --- a/Xcodes/Frontend/MainWindow.swift +++ b/Xcodes/Frontend/MainWindow.swift @@ -20,7 +20,7 @@ struct MainWindow: View { var body: some View { NavigationSplitViewWrapper { XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly) - .frame(minWidth: 300) + .frame(minWidth: 250) .layoutPriority(1) .alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in Alert(title: Text(String(format: localizeString("Alert.Uninstall.Title"), xcode.description)), @@ -37,7 +37,7 @@ struct MainWindow: View { } detail: { Group { if let xcode = xcode { - InfoPane(xcode: xcode) + InfoPane2(xcode: xcode) } else { UnselectedView() } diff --git a/Xcodes/Frontend/XcodeList/MainToolbar.swift b/Xcodes/Frontend/XcodeList/MainToolbar.swift index b21460b..c64c1dd 100644 --- a/Xcodes/Frontend/XcodeList/MainToolbar.swift +++ b/Xcodes/Frontend/XcodeList/MainToolbar.swift @@ -70,18 +70,6 @@ struct MainToolbarModifier: ViewModifier { } .help("FilterInstalledDescription") -// Button(action: { isShowingInfoPane.toggle() }) { -// if isShowingInfoPane { -// Label("Info", systemImage: "info.circle.fill") -// .foregroundColor(.accentColor) -// } else { -// Label("Info", systemImage: "info.circle") -// } -// } -// .keyboardShortcut(KeyboardShortcut("i", modifiers: [.command, .option])) -// .help("InfoDescription") - - } } } diff --git a/Xcodes/Resources/Licenses.rtf b/Xcodes/Resources/Licenses.rtf index 179cdf4..d79d973 100644 --- a/Xcodes/Resources/Licenses.rtf +++ b/Xcodes/Resources/Licenses.rtf @@ -335,6 +335,33 @@ For more information, please refer to <>\ otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.\ \ +\fs34 SwiftUIMasonry\ +\ + +\fs26 MIT License\ +\ +Copyright (c) 2022 Ciaran O'Brien\ +\ +Permission is hereby granted, free of charge, to any person obtaining a copy\ +of this software and associated documentation files (the "Software"), to deal\ +in the Software without restriction, including without limitation the rights\ +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ +copies of the Software, and to permit persons to whom the Software is\ +furnished to do so, subject to the following conditions:\ +\ +The above copyright notice and this permission notice shall be included in all\ +copies or substantial portions of the Software.\ +\ +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ +SOFTWARE.\ +\ +\ + \fs34 DockProgress\ \ diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index c655402..be5230f 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -15467,6 +15467,7 @@ } }, "ReleaseNotes" : { + "extractionState" : "stale", "localizations" : { "ca" : { "stringUnit" : { diff --git a/Xcodes/XcodesApp.swift b/Xcodes/XcodesApp.swift index e735bb8..695a9e2 100644 --- a/Xcodes/XcodesApp.swift +++ b/Xcodes/XcodesApp.swift @@ -23,6 +23,7 @@ struct XcodesApp: App { guard !isTesting else { return } if case .active = newScenePhase { appState.updateIfNeeded() + appState.updateInstalledRuntimes() } } } diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift index a85d3c8..f2ed89b 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift @@ -9,14 +9,28 @@ import Foundation public struct CoreSimulatorPlist: Decodable { public let images: [CoreSimulatorImage] + + public init(images: [CoreSimulatorImage]) { + self.images = images + } } public struct CoreSimulatorImage: Decodable { public let uuid: String public let path: [String: String] public let runtimeInfo: CoreSimulatorRuntimeInfo + + public init(uuid: String, path: [String : String], runtimeInfo: CoreSimulatorRuntimeInfo) { + self.uuid = uuid + self.path = path + self.runtimeInfo = runtimeInfo + } } public struct CoreSimulatorRuntimeInfo: Decodable { public let build: String + + public init(build: String) { + self.build = build + } }