diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 6c418b84..078e3507 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -114,8 +114,9 @@ E689540325BE8C64000EBCEA /* DockProgress in Frameworks */ = {isa = PBXBuildFile; productRef = E689540225BE8C64000EBCEA /* DockProgress */; }; E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.swift */; }; E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */; }; - E84CF8C12B0FEB8300ECA259 /* RuntimesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84CF8C02B0FEB8300ECA259 /* RuntimesView.swift */; }; - E872EE4E2808D4F100D3DD8B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E872EE502808D4F100D3DD8B /* Localizable.strings */; }; + E84B7D0D2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */; }; + E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F512B323A5F003F3959 /* CornerRadiusModifier.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 */; }; @@ -310,9 +311,10 @@ CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = ""; }; E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = ""; }; E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeInstallationStepDetailView.swift; sourceTree = ""; }; - E84CF8C02B0FEB8300ECA259 /* RuntimesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimesView.swift; sourceTree = ""; }; + E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitViewWrapper.swift; sourceTree = ""; }; + E84E4F512B323A5F003F3959 /* CornerRadiusModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerRadiusModifier.swift; sourceTree = ""; }; E856BB73291EDD3D00DC438B /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = ""; }; - E872EE4F2808D4F100D3DD8B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; 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 = ""; }; @@ -373,6 +375,7 @@ CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */, 536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */, 53CBAB2B263DCC9100410495 /* XcodesAlert.swift */, + E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */, ); path = Common; sourceTree = ""; @@ -646,7 +649,8 @@ B0C6AD0A2AD9178E00E64698 /* IdenticalBuildView.swift */, B0C6AD0C2AD91D7900E64698 /* IconView.swift */, E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */, - E84CF8C02B0FEB8300ECA259 /* RuntimesView.swift */, + E86671262B309D2F0048559A /* PlatformsView.swift */, + E84E4F512B323A5F003F3959 /* CornerRadiusModifier.swift */, ); path = InfoPane; sourceTree = ""; @@ -868,7 +872,6 @@ CA9FF8CF25959A9700E47BAF /* HelperXPCShared.swift in Sources */, CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */, CAFBDC4E2599B33D003DCC5A /* MainToolbar.swift in Sources */, - E84CF8C12B0FEB8300ECA259 /* RuntimesView.swift in Sources */, CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */, CAA8589B25A2B83000ACF8C0 /* Aria2CError.swift in Sources */, 536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */, @@ -897,6 +900,7 @@ B0403CFC2AD9A6BF00137C09 /* InstalledStateButtons.swift in Sources */, 36741BFF291E50F500A85AAE /* FileError.swift in Sources */, CA9FF8872595607900E47BAF /* InstalledXcode.swift in Sources */, + E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */, B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */, B0403CFE2ADA712C00137C09 /* InfoPaneControls.swift in Sources */, 53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */, @@ -910,6 +914,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 */, @@ -921,6 +926,7 @@ E8B20CBF2A2EDEC20057D816 /* SDKs+Xcode.swift in Sources */, CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */, CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */, + E84B7D0D2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift in Sources */, CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */, CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */, B0403CF42AD9381D00137C09 /* SDKsView.swift in Sources */, @@ -1040,7 +1046,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1227,7 +1233,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1285,7 +1291,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index f39c1050..9143667b 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 7de4af74..998e9f82 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 1462b963..e01f3b47 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 97f36b39..b168bd4d 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/Common/NavigationSplitViewWrapper.swift b/Xcodes/Frontend/Common/NavigationSplitViewWrapper.swift new file mode 100644 index 00000000..928e49a8 --- /dev/null +++ b/Xcodes/Frontend/Common/NavigationSplitViewWrapper.swift @@ -0,0 +1,46 @@ +// +// NavigationSplitViewWrapper.swift +// Xcodes +// +// Created by Matt Kiazyk on 2023-12-12. +// + +import SwiftUI + +struct NavigationSplitViewWrapper: View where Sidebar: View, Detail: View { + private var sidebar: Sidebar + private var detail: Detail + + init( + @ViewBuilder sidebar: () -> Sidebar, + @ViewBuilder detail: () -> Detail + ) { + self.sidebar = sidebar() + self.detail = detail() + } + + var body: some View { + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, visionOS 1, *) { + // Use the latest API available + NavigationSplitView { + + if #available(macOS 14, *) { + sidebar + .toolbar(removing: .sidebarToggle) + } else { + sidebar + } + } detail: { + detail + } + } else { + // Alternative code for earlier versions of OS. + NavigationView { + // The first column is the sidebar. + sidebar + detail + } + .navigationViewStyle(.columns) + } + } +} diff --git a/Xcodes/Frontend/InfoPane/CompatibilityView.swift b/Xcodes/Frontend/InfoPane/CompatibilityView.swift index 2cf3b40c..f5a583b7 100644 --- a/Xcodes/Frontend/InfoPane/CompatibilityView.swift +++ b/Xcodes/Frontend/InfoPane/CompatibilityView.swift @@ -9,16 +9,27 @@ 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) + } } + .xcodesBackground() } else { EmptyView() } @@ -28,4 +39,5 @@ struct CompatibilityView: View { #Preview { CompatibilityView(requiredMacOSVersion: "10.15.4") .padding() + .environmentObject(AppState()) } diff --git a/Xcodes/Frontend/InfoPane/CornerRadiusModifier.swift b/Xcodes/Frontend/InfoPane/CornerRadiusModifier.swift new file mode 100644 index 00000000..4d9d7e27 --- /dev/null +++ b/Xcodes/Frontend/InfoPane/CornerRadiusModifier.swift @@ -0,0 +1,35 @@ +// +// CornerRadiusModifier.swift +// Xcodes +// +// Created by Matt Kiazyk on 2023-12-19. +// + +import Foundation +import SwiftUI + +struct CornerRadiusModifier: ViewModifier { + func body(content: Content) -> some View { + content + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(.background) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) + } +} + +extension View { + func xcodesBackground() -> some View { + self.modifier( + CornerRadiusModifier() + ) + } +} + +struct Previews_CornerRadius_Previews: PreviewProvider { + static var previews: some View { + HStack { + Text("XCODES RULES!") + }.xcodesBackground() + } +} diff --git a/Xcodes/Frontend/InfoPane/IdenticalBuildView.swift b/Xcodes/Frontend/InfoPane/IdenticalBuildView.swift index 2eac1f06..04ef3b82 100644 --- a/Xcodes/Frontend/InfoPane/IdenticalBuildView.swift +++ b/Xcodes/Frontend/InfoPane/IdenticalBuildView.swift @@ -33,6 +33,7 @@ struct IdenticalBuildsView: View { .font(.subheadline) } } + .xcodesBackground() .accessibilityElement() .accessibility(label: Text("IdenticalBuilds")) .accessibility(value: Text(accessibilityDescription)) diff --git a/Xcodes/Frontend/InfoPane/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift index d39648a7..36f33872 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 @@ -7,44 +8,65 @@ import struct XCModel.SDKs struct InfoPane: View { let xcode: Xcode - var body: some View { - 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) - - InfoPaneControls(xcode: xcode) - - Divider() - - Group { - RuntimesView(xcode: xcode) - ReleaseDateView(date: xcode.releaseDate) - ReleaseNotesView(url: xcode.releaseNotesURL) - IdenticalBuildsView(builds: xcode.identicalBuilds) + 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) + } + .xcodesBackground() + + + VStack { + Text("Platforms") + .font(.title3) + .frame(maxWidth: .infinity, alignment: .leading) + PlatformsView(xcode: xcode) + } + .xcodesBackground() + } + + VStack(alignment: .leading) { + ReleaseDateView(date: xcode.releaseDate, url: xcode.releaseNotesURL) CompatibilityView(requiredMacOSVersion: xcode.requiredMacOSVersion) - SDKsView(sdks: xcode.sdks) - CompilersView(compilers: xcode.compilers) + IdenticalBuildsView(builds: xcode.identicalBuilds) + SDKandCompilers } - - Spacer() + .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(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 +75,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 +143,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/InfoPaneControls.swift b/Xcodes/Frontend/InfoPane/InfoPaneControls.swift index 6034a38e..73cdb35b 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 00000000..184e597d --- /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 c7ee3043..838c3659 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 10e3638a..8f16e3a1 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/InfoPane/RuntimesView.swift b/Xcodes/Frontend/InfoPane/RuntimesView.swift deleted file mode 100644 index 5148c716..00000000 --- a/Xcodes/Frontend/InfoPane/RuntimesView.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// RuntimesView.swift -// Xcodes -// -// Created by Matt Kiazyk on 2023-11-23. -// Copyright © 2023 Robots and Pencils. All rights reserved. -// - -import SwiftUI -import XcodesKit - -struct RuntimesView: View { - @EnvironmentObject var appState: AppState - let xcode: Xcode - - var body: 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 - } - } - - ForEach(runtimes ?? [], id: \.simulatorVersion.buildUpdate) { runtime in - VStack { - runtimeRow(runtime: runtime) - } - - } - } - } - - @ViewBuilder - func runtimeRow(runtime: DownloadableRuntime) -> some View { - HStack { - Text("\(runtime.visibleIdentifier)") - .font(.subheadline) - Spacer() - Text(runtime.downloadFileSizeString) - .font(.subheadline) - - switch runtime.installState { - case .installed, .notInstalled: - // 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) - } - case .installing(_): - CancelRuntimeInstallButton(runtime: runtime) - } - - } - - switch runtime.installState { - - case .installing(let installationStep): - RuntimeInstallationStepDetailView(installationStep: installationStep) - .fixedSize(horizontal: false, vertical: true) - default: - EmptyView() - } - } -} - -//#Preview { -// RuntimesView() -//} diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift index b8df7748..cee42acb 100644 --- a/Xcodes/Frontend/MainWindow.swift +++ b/Xcodes/Frontend/MainWindow.swift @@ -1,6 +1,8 @@ import ErrorHandling import SwiftUI import XcodesKit +import Path +import Version struct MainWindow: View { @EnvironmentObject var appState: AppState @@ -16,9 +18,9 @@ struct MainWindow: View { @AppStorage("isInstalledOnly") private var isInstalledOnly = false var body: some View { - HSplitView { + 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)), @@ -26,25 +28,73 @@ struct MainWindow: View { primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(xcode: xcode) }), secondaryButton: .cancel(Text("Cancel"))) } - - if isShowingInfoPane { - Group { - if let xcode = xcode { - InfoPane(xcode: xcode) + .searchable(text: $searchText, placement: .sidebar) + .mainToolbar( + category: $category, + isInstalledOnly: $isInstalledOnly, + isShowingInfoPane: $isShowingInfoPane + ) + } detail: { + Group { + if let xcode = xcode { + InfoPane(xcode: xcode) + } else { + UnselectedView() + } + } + .padding() + .toolbar { + ToolbarItemGroup { + Button(action: { appState.presentedSheet = .signIn }, label: { + Label("Login", systemImage: "person.circle") + }) + .help("LoginDescription") + if #available(macOS 14, *) { + SettingsLink(label: { + Label("Preferences", systemImage: "gearshape") + }) + .help("PreferencesDescription") } else { - UnselectedView() + Button(action: { + if #available(macOS 13, *) { + NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) + } else { + NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) + } + }, label: { + Label("Preferences", systemImage: "gearshape") + }) + .help("PreferencesDescription") } } - .padding() - .frame(minWidth: 300, maxWidth: .infinity) } } - .mainToolbar( - category: $category, - isInstalledOnly: $isInstalledOnly, - isShowingInfoPane: $isShowingInfoPane, - searchText: $searchText - ) + +// HSplitView { +// XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly) +// .frame(minWidth: 300) +// .layoutPriority(1) +// .alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in +// Alert(title: Text(String(format: localizeString("Alert.Uninstall.Title"), xcode.description)), +// message: Text("Alert.Uninstall.Message"), +// primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(xcode: xcode) }), +// secondaryButton: .cancel(Text("Cancel"))) +// } +// .searchable(text: $searchText) +// +// if isShowingInfoPane { +// Group { +// if let xcode = xcode { +// InfoPane(xcode: xcode) +// } else { +// UnselectedView() +// } +// } +// .padding() +// .frame(minWidth: 300, maxWidth: .infinity) +// } +// } + .bottomStatusBar() .padding([.top], 0) .navigationSubtitle(subtitleText) @@ -197,6 +247,16 @@ struct MainWindow: View { struct MainWindow_Previews: PreviewProvider { static var previews: some View { - MainWindow() + MainWindow().environmentObject({ () -> AppState in + let a = AppState() + a.allXcodes = [ + Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [Version("12.0.0+1234A")!, Version("12.0.0-RC+1234A")!], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil), + Xcode(version: Version("12.3.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: true, icon: nil), + Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, icon: nil), + Xcode(version: Version("12.1.0")!, installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 })), selected: false, icon: nil), + Xcode(version: Version("12.0.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil), + ] + return a + }()) } } diff --git a/Xcodes/Frontend/XcodeList/MainToolbar.swift b/Xcodes/Frontend/XcodeList/MainToolbar.swift index e3553045..c64c1dd5 100644 --- a/Xcodes/Frontend/XcodeList/MainToolbar.swift +++ b/Xcodes/Frontend/XcodeList/MainToolbar.swift @@ -5,7 +5,6 @@ struct MainToolbarModifier: ViewModifier { @Binding var category: XcodeListCategory @Binding var isInstalledOnly: Bool @Binding var isShowingInfoPane: Bool - @Binding var searchText: String func body(content: Content) -> some View { content @@ -14,10 +13,6 @@ struct MainToolbarModifier: ViewModifier { private var toolbar: some ToolbarContent { ToolbarItemGroup { - Button(action: { appState.presentedSheet = .signIn }, label: { - Label("Login", systemImage: "person.circle") - }) - .help("LoginDescription") ProgressButton( isInProgress: appState.isUpdating, @@ -27,7 +22,7 @@ struct MainToolbarModifier: ViewModifier { } .keyboardShortcut(KeyEquivalent("r")) .help("RefreshDescription") - + Spacer() Button(action: { switch category { case .all: category = .release @@ -75,39 +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") - - if #available(macOS 14, *) { - SettingsLink(label: { - Label("Preferences", systemImage: "gearshape") - }) - .help("PreferencesDescription") - } else { - Button(action: { - if #available(macOS 13, *) { - NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) - } else { - NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) - } - }, label: { - Label("Preferences", systemImage: "gearshape") - }) - .help("PreferencesDescription") - } - - TextField("Search", text: $searchText) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .frame(width: 200) - .help("SearchDescription") } } } @@ -116,15 +78,13 @@ extension View { func mainToolbar( category: Binding, isInstalledOnly: Binding, - isShowingInfoPane: Binding, - searchText: Binding + isShowingInfoPane: Binding ) -> some View { self.modifier( MainToolbarModifier( category: category, isInstalledOnly: isInstalledOnly, - isShowingInfoPane: isShowingInfoPane, - searchText: searchText + isShowingInfoPane: isShowingInfoPane ) ) } diff --git a/Xcodes/Frontend/XcodeList/XcodeListView.swift b/Xcodes/Frontend/XcodeList/XcodeListView.swift index 101010f3..fdbdfc8e 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListView.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListView.swift @@ -42,6 +42,7 @@ struct XcodeListView: View { List(visibleXcodes, selection: $selectedXcodeID) { xcode in XcodeListViewRow(xcode: xcode, selected: selectedXcodeID == xcode.id, appState: appState) } + .listStyle(.sidebar) } } diff --git a/Xcodes/Resources/Licenses.rtf b/Xcodes/Resources/Licenses.rtf index 179cdf42..d79d9738 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 c27943da..b7d3d62e 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -8193,6 +8193,7 @@ } }, "Info" : { + "extractionState" : "stale", "localizations" : { "ca" : { "stringUnit" : { @@ -8299,6 +8300,7 @@ } }, "InfoDescription" : { + "extractionState" : "stale", "localizations" : { "ca" : { "stringUnit" : { @@ -15465,6 +15467,7 @@ } }, "ReleaseNotes" : { + "extractionState" : "stale", "localizations" : { "ca" : { "stringUnit" : { @@ -16125,6 +16128,7 @@ } }, "Search" : { + "extractionState" : "stale", "localizations" : { "ca" : { "stringUnit" : { @@ -16237,6 +16241,7 @@ } }, "SearchDescription" : { + "extractionState" : "stale", "localizations" : { "ca" : { "stringUnit" : { @@ -18438,6 +18443,9 @@ } } } + }, + "XCODES RULES!" : { + } }, "version" : "1.0" diff --git a/Xcodes/XcodesApp.swift b/Xcodes/XcodesApp.swift index e735bb87..695a9e23 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 a85d3c89..f2ed89b9 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 + } }