diff --git a/Mist.xcodeproj/project.pbxproj b/Mist.xcodeproj/project.pbxproj index 227c3f9..02e3533 100644 --- a/Mist.xcodeproj/project.pbxproj +++ b/Mist.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ 390451BF2856E34700E0B563 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390451BE2856E34700E0B563 /* String+Extension.swift */; }; 390451C22856E3F500E0B563 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390451C12856E3F500E0B563 /* Hardware.swift */; }; 390451C62856E80C00E0B563 /* RefreshView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390451C52856E80C00E0B563 /* RefreshView.swift */; }; - 390451C82856E94900E0B563 /* FirmwareListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390451C72856E94900E0B563 /* FirmwareListRow.swift */; }; + 390451C82856E94900E0B563 /* ListRowFirmware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390451C72856E94900E0B563 /* ListRowFirmware.swift */; }; 390451CA2856F1D300E0B563 /* ScaledImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390451C92856F1D300E0B563 /* ScaledImage.swift */; }; 390451CC2856F23100E0B563 /* ScaledSystemImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390451CB2856F23100E0B563 /* ScaledSystemImage.swift */; }; 390451CE2856F42800E0B563 /* DownloadType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390451CD2856F42800E0B563 /* DownloadType.swift */; }; @@ -53,7 +53,7 @@ 39252AB7285C718C00956C74 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39252AB6285C718C00956C74 /* FileManager+Extension.swift */; }; 39252AB9285C7BC700956C74 /* SettingsInstallersCacheView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39252AB8285C7BC700956C74 /* SettingsInstallersCacheView.swift */; }; 39252ABB285C7D3800956C74 /* SettingsInstallersCatalogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39252ABA285C7D3800956C74 /* SettingsInstallersCatalogsView.swift */; }; - 39252ABD285C8FFC00956C74 /* InstallerListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39252ABC285C8FFC00956C74 /* InstallerListRow.swift */; }; + 39252ABD285C8FFC00956C74 /* ListRowInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39252ABC285C8FFC00956C74 /* ListRowInstaller.swift */; }; 39252AC3285CA5FE00956C74 /* InstallerExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39252AC2285CA5FE00956C74 /* InstallerExportView.swift */; }; 3935F47428643AB800760AB0 /* UNNotificationCategory+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F47328643AB800760AB0 /* UNNotificationCategory+Extension.swift */; }; 3935F47628643AF000760AB0 /* UNNotificationAction+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F47528643AF000760AB0 /* UNNotificationAction+Extension.swift */; }; @@ -62,22 +62,21 @@ 3935F47E2864813B00760AB0 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F47D2864813B00760AB0 /* DownloadManager.swift */; }; 3935F480286551FB00760AB0 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F47F286551FB00760AB0 /* Double+Extension.swift */; }; 3935F4852866B64900760AB0 /* MistTaskSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4842866B64900760AB0 /* MistTaskSection.swift */; }; - 3935F4892866C68000760AB0 /* DownloadSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4882866C68000760AB0 /* DownloadSectionHeaderView.swift */; }; + 3935F4892866C68000760AB0 /* ActivitySectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4882866C68000760AB0 /* ActivitySectionHeaderView.swift */; }; 3935F48E2869278200760AB0 /* InstallerExportType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F48D2869278100760AB0 /* InstallerExportType.swift */; }; 3935F490286976D000760AB0 /* ProgressAlertType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F48F286976D000760AB0 /* ProgressAlertType.swift */; }; 3935F49D286ABE4D00760AB0 /* FooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F49C286ABE4D00760AB0 /* FooterView.swift */; }; - 3935F49F286AC32C00760AB0 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F49E286AC32C00760AB0 /* ListRow.swift */; }; 3935F4A2286ACD4D00760AB0 /* InstallerExportViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4A1286ACD4D00760AB0 /* InstallerExportViewItem.swift */; }; - 3935F4A4286AD21000760AB0 /* DownloadProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4A3286AD21000760AB0 /* DownloadProgressView.swift */; }; - 3935F4A6286AD3E100760AB0 /* DownloadHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4A5286AD3E100760AB0 /* DownloadHeaderView.swift */; }; - 3935F4A8286AD5D000760AB0 /* DownloadRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4A7286AD5D000760AB0 /* DownloadRowView.swift */; }; + 3935F4A4286AD21000760AB0 /* ActivityProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4A3286AD21000760AB0 /* ActivityProgressView.swift */; }; + 3935F4A6286AD3E100760AB0 /* ActivityHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4A5286AD3E100760AB0 /* ActivityHeaderView.swift */; }; + 3935F4A8286AD5D000760AB0 /* ActivityRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4A7286AD5D000760AB0 /* ActivityRowView.swift */; }; 3935F4AB286B04BC00760AB0 /* HelperToolInfoPropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4A9286B04BC00760AB0 /* HelperToolInfoPropertyList.swift */; }; 3935F4AC286B04BC00760AB0 /* HelperToolLaunchdPropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4AA286B04BC00760AB0 /* HelperToolLaunchdPropertyList.swift */; }; 3935F4AD286B04BF00760AB0 /* HelperToolInfoPropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4A9286B04BC00760AB0 /* HelperToolInfoPropertyList.swift */; }; 3935F4AE286B04BF00760AB0 /* HelperToolLaunchdPropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4AA286B04BC00760AB0 /* HelperToolLaunchdPropertyList.swift */; }; 3935F4C5286B546A00760AB0 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 3935F4C4286B546A00760AB0 /* Sparkle */; }; 3935F4C7286B54E200760AB0 /* SparkleUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4C6286B54E200760AB0 /* SparkleUpdater.swift */; }; - 3935F4CB286C1EC500760AB0 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4CA286C1EC500760AB0 /* DownloadView.swift */; }; + 3935F4CB286C1EC500760AB0 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4CA286C1EC500760AB0 /* ActivityView.swift */; }; 3935F4CD286C6A5D00760AB0 /* ProcessKiller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3935F4CC286C6A5D00760AB0 /* ProcessKiller.swift */; }; 393D8029286EB4D6008AA8E3 /* EmptyCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393D8028286EB4D6008AA8E3 /* EmptyCollectionView.swift */; }; 393F35B928640DF6005B7165 /* ShellExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39252A9E285C140D00956C74 /* ShellExecutor.swift */; }; @@ -85,7 +84,7 @@ 393F35BC28641181005B7165 /* RefreshState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393F35BB28641181005B7165 /* RefreshState.swift */; }; 393F35BE2864197F005B7165 /* PrivilegedHelperTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393F35BD2864197F005B7165 /* PrivilegedHelperTool.swift */; }; 393F35C228641E1F005B7165 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393F35C128641E1F005B7165 /* HeaderView.swift */; }; - 395DCD16287FE36E00C411CE /* DownloadAlertType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 395DCD15287FE36E00C411CE /* DownloadAlertType.swift */; }; + 395DCD16287FE36E00C411CE /* InstallerAlertType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 395DCD15287FE36E00C411CE /* InstallerAlertType.swift */; }; 398734C428600E6E00B4C357 /* TaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398734C328600E6E00B4C357 /* TaskManager.swift */; }; 398734C6286011C300B4C357 /* Validator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398734C5286011C300B4C357 /* Validator.swift */; }; 398734C828601FFC00B4C357 /* FileMover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398734C728601FFC00B4C357 /* FileMover.swift */; }; @@ -132,11 +131,15 @@ 39FF05FA285985DD00A86670 /* SettingsAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39FF05F9285985DD00A86670 /* SettingsAboutView.swift */; }; 573A23622A28711C00EC9470 /* Architecture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 573A23612A28711C00EC9470 /* Architecture.swift */; }; 573A23642A28791F00EC9470 /* Scene+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 573A23632A28791F00EC9470 /* Scene+Extension.swift */; }; - 575812B72A372D7200425BAF /* CapsuleButtonStyleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812B62A372D7200425BAF /* CapsuleButtonStyleType.swift */; }; + 575812BA2A373A4F00425BAF /* FirmwareAlertType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812B92A373A4F00425BAF /* FirmwareAlertType.swift */; }; + 575812BC2A37406300425BAF /* ListRowDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812BB2A37406300425BAF /* ListRowDetail.swift */; }; + 575812BE2A3743E300425BAF /* InstallerSheetType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812BD2A3743E300425BAF /* InstallerSheetType.swift */; }; + 575812C02A37493F00425BAF /* InstallerVolumeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812BF2A37493F00425BAF /* InstallerVolumeSelectionView.swift */; }; + 575812C22A380B5E00425BAF /* InstallerVolume.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812C12A380B5E00425BAF /* InstallerVolume.swift */; }; + 575812C42A3821A900425BAF /* InstallerVolumeSelectionInformationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812C32A3821A900425BAF /* InstallerVolumeSelectionInformationView.swift */; }; + 575812C62A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812C52A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift */; }; 5795700B2A31B06F004C7051 /* ButtonStyle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5795700A2A31B06F004C7051 /* ButtonStyle+Extension.swift */; }; - 5795700D2A31B081004C7051 /* CapsuleButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5795700C2A31B081004C7051 /* CapsuleButtonStyle.swift */; }; - 57CF961A2A34B65C008D3B1C /* CapsuleLeading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57CF96192A34B65C008D3B1C /* CapsuleLeading.swift */; }; - 57CF961C2A34B9E0008D3B1C /* CapsuleTrailing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57CF961B2A34B9E0008D3B1C /* CapsuleTrailing.swift */; }; + 5795700D2A31B081004C7051 /* MistActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5795700C2A31B081004C7051 /* MistActionButtonStyle.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -170,7 +173,7 @@ 390451BE2856E34700E0B563 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 390451C12856E3F500E0B563 /* Hardware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hardware.swift; sourceTree = ""; }; 390451C52856E80C00E0B563 /* RefreshView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshView.swift; sourceTree = ""; }; - 390451C72856E94900E0B563 /* FirmwareListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareListRow.swift; sourceTree = ""; }; + 390451C72856E94900E0B563 /* ListRowFirmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowFirmware.swift; sourceTree = ""; }; 390451C92856F1D300E0B563 /* ScaledImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledImage.swift; sourceTree = ""; }; 390451CB2856F23100E0B563 /* ScaledSystemImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledSystemImage.swift; sourceTree = ""; }; 390451CD2856F42800E0B563 /* DownloadType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadType.swift; sourceTree = ""; }; @@ -208,7 +211,7 @@ 39252AB6285C718C00956C74 /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = ""; }; 39252AB8285C7BC700956C74 /* SettingsInstallersCacheView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInstallersCacheView.swift; sourceTree = ""; }; 39252ABA285C7D3800956C74 /* SettingsInstallersCatalogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInstallersCatalogsView.swift; sourceTree = ""; }; - 39252ABC285C8FFC00956C74 /* InstallerListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerListRow.swift; sourceTree = ""; }; + 39252ABC285C8FFC00956C74 /* ListRowInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowInstaller.swift; sourceTree = ""; }; 39252AC2285CA5FE00956C74 /* InstallerExportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerExportView.swift; sourceTree = ""; }; 3935F47328643AB800760AB0 /* UNNotificationCategory+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotificationCategory+Extension.swift"; sourceTree = ""; }; 3935F47528643AF000760AB0 /* UNNotificationAction+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotificationAction+Extension.swift"; sourceTree = ""; }; @@ -217,26 +220,25 @@ 3935F47D2864813B00760AB0 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; 3935F47F286551FB00760AB0 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; 3935F4842866B64900760AB0 /* MistTaskSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MistTaskSection.swift; sourceTree = ""; }; - 3935F4882866C68000760AB0 /* DownloadSectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadSectionHeaderView.swift; sourceTree = ""; }; + 3935F4882866C68000760AB0 /* ActivitySectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivitySectionHeaderView.swift; sourceTree = ""; }; 3935F48D2869278100760AB0 /* InstallerExportType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstallerExportType.swift; sourceTree = ""; }; 3935F48F286976D000760AB0 /* ProgressAlertType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressAlertType.swift; sourceTree = ""; }; 3935F49C286ABE4D00760AB0 /* FooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FooterView.swift; sourceTree = ""; }; - 3935F49E286AC32C00760AB0 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = ""; }; 3935F4A1286ACD4D00760AB0 /* InstallerExportViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerExportViewItem.swift; sourceTree = ""; }; - 3935F4A3286AD21000760AB0 /* DownloadProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgressView.swift; sourceTree = ""; }; - 3935F4A5286AD3E100760AB0 /* DownloadHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadHeaderView.swift; sourceTree = ""; }; - 3935F4A7286AD5D000760AB0 /* DownloadRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadRowView.swift; sourceTree = ""; }; + 3935F4A3286AD21000760AB0 /* ActivityProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityProgressView.swift; sourceTree = ""; }; + 3935F4A5286AD3E100760AB0 /* ActivityHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityHeaderView.swift; sourceTree = ""; }; + 3935F4A7286AD5D000760AB0 /* ActivityRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityRowView.swift; sourceTree = ""; }; 3935F4A9286B04BC00760AB0 /* HelperToolInfoPropertyList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelperToolInfoPropertyList.swift; sourceTree = ""; }; 3935F4AA286B04BC00760AB0 /* HelperToolLaunchdPropertyList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelperToolLaunchdPropertyList.swift; sourceTree = ""; }; 3935F4AF286B195E00760AB0 /* launchd.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = launchd.plist; sourceTree = ""; }; 3935F4C6286B54E200760AB0 /* SparkleUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparkleUpdater.swift; sourceTree = ""; }; - 3935F4CA286C1EC500760AB0 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = ""; }; + 3935F4CA286C1EC500760AB0 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; 3935F4CC286C6A5D00760AB0 /* ProcessKiller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessKiller.swift; sourceTree = ""; }; 393D8028286EB4D6008AA8E3 /* EmptyCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyCollectionView.swift; sourceTree = ""; }; 393F35BB28641181005B7165 /* RefreshState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshState.swift; sourceTree = ""; }; 393F35BD2864197F005B7165 /* PrivilegedHelperTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivilegedHelperTool.swift; sourceTree = ""; }; 393F35C128641E1F005B7165 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; - 395DCD15287FE36E00C411CE /* DownloadAlertType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadAlertType.swift; sourceTree = ""; }; + 395DCD15287FE36E00C411CE /* InstallerAlertType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerAlertType.swift; sourceTree = ""; }; 398734C328600E6E00B4C357 /* TaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskManager.swift; sourceTree = ""; }; 398734C5286011C300B4C357 /* Validator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validator.swift; sourceTree = ""; }; 398734C728601FFC00B4C357 /* FileMover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMover.swift; sourceTree = ""; }; @@ -279,11 +281,15 @@ 39FF05F9285985DD00A86670 /* SettingsAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAboutView.swift; sourceTree = ""; }; 573A23612A28711C00EC9470 /* Architecture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Architecture.swift; sourceTree = ""; }; 573A23632A28791F00EC9470 /* Scene+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Scene+Extension.swift"; sourceTree = ""; }; - 575812B62A372D7200425BAF /* CapsuleButtonStyleType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsuleButtonStyleType.swift; sourceTree = ""; }; + 575812B92A373A4F00425BAF /* FirmwareAlertType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareAlertType.swift; sourceTree = ""; }; + 575812BB2A37406300425BAF /* ListRowDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowDetail.swift; sourceTree = ""; }; + 575812BD2A3743E300425BAF /* InstallerSheetType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerSheetType.swift; sourceTree = ""; }; + 575812BF2A37493F00425BAF /* InstallerVolumeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerVolumeSelectionView.swift; sourceTree = ""; }; + 575812C12A380B5E00425BAF /* InstallerVolume.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerVolume.swift; sourceTree = ""; }; + 575812C32A3821A900425BAF /* InstallerVolumeSelectionInformationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerVolumeSelectionInformationView.swift; sourceTree = ""; }; + 575812C52A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerVolumeSelectionPickerView.swift; sourceTree = ""; }; 5795700A2A31B06F004C7051 /* ButtonStyle+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ButtonStyle+Extension.swift"; sourceTree = ""; }; - 5795700C2A31B081004C7051 /* CapsuleButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapsuleButtonStyle.swift; sourceTree = ""; }; - 57CF96192A34B65C008D3B1C /* CapsuleLeading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsuleLeading.swift; sourceTree = ""; }; - 57CF961B2A34B9E0008D3B1C /* CapsuleTrailing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsuleTrailing.swift; sourceTree = ""; }; + 5795700C2A31B081004C7051 /* MistActionButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MistActionButtonStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -412,13 +418,16 @@ 39CB5E3E2941486D00CFDBB8 /* CatalogSeedType.swift */, 398734CB28603D5F00B4C357 /* Chunklist.swift */, 398734CD28603D7F00B4C357 /* Chunk.swift */, - 395DCD15287FE36E00C411CE /* DownloadAlertType.swift */, 390451CD2856F42800E0B563 /* DownloadType.swift */, 390451D928573ADC00E0B563 /* ExportListType.swift */, 390451B82856E24200E0B563 /* Firmware.swift */, + 575812B92A373A4F00425BAF /* FirmwareAlertType.swift */, 390451C12856E3F500E0B563 /* Hardware.swift */, 390451CF2856F63700E0B563 /* Installer.swift */, + 395DCD15287FE36E00C411CE /* InstallerAlertType.swift */, + 575812BD2A3743E300425BAF /* InstallerSheetType.swift */, 3935F48D2869278100760AB0 /* InstallerExportType.swift */, + 575812C12A380B5E00425BAF /* InstallerVolume.swift */, 39252A9A285C029600956C74 /* MistError.swift */, 39252A94285BF83D00956C74 /* MistTask.swift */, 3935F4842866B64900760AB0 /* MistTaskSection.swift */, @@ -440,7 +449,7 @@ 3935F49C286ABE4D00760AB0 /* FooterView.swift */, 390451D728573A2500E0B563 /* ExportListView.swift */, 3935F4A0286ACCE100760AB0 /* List */, - 393F35BF28641D86005B7165 /* Download */, + 393F35BF28641D86005B7165 /* Activity */, 393F35C028641D8F005B7165 /* Refresh */, 39FF05F2285984F800A86670 /* Settings */, 39FF05F12859849200A86670 /* Components */, @@ -451,25 +460,28 @@ 3935F4A0286ACCE100760AB0 /* List */ = { isa = PBXGroup; children = ( - 390451C72856E94900E0B563 /* FirmwareListRow.swift */, - 39252ABC285C8FFC00956C74 /* InstallerListRow.swift */, - 3935F49E286AC32C00760AB0 /* ListRow.swift */, + 390451C72856E94900E0B563 /* ListRowFirmware.swift */, + 39252ABC285C8FFC00956C74 /* ListRowInstaller.swift */, + 575812BB2A37406300425BAF /* ListRowDetail.swift */, 39252AC2285CA5FE00956C74 /* InstallerExportView.swift */, 3935F4A1286ACD4D00760AB0 /* InstallerExportViewItem.swift */, + 575812BF2A37493F00425BAF /* InstallerVolumeSelectionView.swift */, + 575812C52A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift */, + 575812C32A3821A900425BAF /* InstallerVolumeSelectionInformationView.swift */, ); path = List; sourceTree = ""; }; - 393F35BF28641D86005B7165 /* Download */ = { + 393F35BF28641D86005B7165 /* Activity */ = { isa = PBXGroup; children = ( - 3935F4CA286C1EC500760AB0 /* DownloadView.swift */, - 3935F4A5286AD3E100760AB0 /* DownloadHeaderView.swift */, - 3935F4882866C68000760AB0 /* DownloadSectionHeaderView.swift */, - 3935F4A7286AD5D000760AB0 /* DownloadRowView.swift */, - 3935F4A3286AD21000760AB0 /* DownloadProgressView.swift */, + 3935F4CA286C1EC500760AB0 /* ActivityView.swift */, + 3935F4A5286AD3E100760AB0 /* ActivityHeaderView.swift */, + 3935F4882866C68000760AB0 /* ActivitySectionHeaderView.swift */, + 3935F4A7286AD5D000760AB0 /* ActivityRowView.swift */, + 3935F4A3286AD21000760AB0 /* ActivityProgressView.swift */, ); - path = Download; + path = Activity; sourceTree = ""; }; 393F35C028641D8F005B7165 /* Refresh */ = { @@ -516,10 +528,10 @@ 39FF05F12859849200A86670 /* Components */ = { isa = PBXGroup; children = ( - 575812B82A37330200425BAF /* Capsule */, 39252AA2285C3CC400956C74 /* CodesigningPickerView.swift */, 39252AA4285C463A00956C74 /* DynamicTextView.swift */, 39252A86285ACE9C00956C74 /* FooterText.swift */, + 5795700C2A31B081004C7051 /* MistActionButtonStyle.swift */, 39252AA0285C2A1600956C74 /* PaddedDivider.swift */, 39148CFB28DD55B300011FF5 /* PathControl.swift */, 39252A84285ACDC800956C74 /* ResetToDefaultButton.swift */, @@ -554,17 +566,6 @@ path = Settings; sourceTree = ""; }; - 575812B82A37330200425BAF /* Capsule */ = { - isa = PBXGroup; - children = ( - 57CF96192A34B65C008D3B1C /* CapsuleLeading.swift */, - 5795700C2A31B081004C7051 /* CapsuleButtonStyle.swift */, - 575812B62A372D7200425BAF /* CapsuleButtonStyleType.swift */, - 57CF961B2A34B9E0008D3B1C /* CapsuleTrailing.swift */, - ); - path = Capsule; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -765,6 +766,7 @@ files = ( 39FF05F02859848500A86670 /* SettingsView.swift in Sources */, 39CF562A2861E1CB006FB5D2 /* DirectoryRemover.swift in Sources */, + 575812C62A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift in Sources */, 398734C828601FFC00B4C357 /* FileMover.swift in Sources */, 39CF55AF2861582F006FB5D2 /* AuthorizationError+Extension.swift in Sources */, 39CF56172861BE66006FB5D2 /* FileCopier.swift in Sources */, @@ -774,12 +776,13 @@ 39CF56212861C992006FB5D2 /* DiskImageMounter.swift in Sources */, 398734CC28603D5F00B4C357 /* Chunklist.swift in Sources */, 39252A83285ACBF200956C74 /* TextFieldStepperView.swift in Sources */, - 3935F4A8286AD5D000760AB0 /* DownloadRowView.swift in Sources */, + 3935F4A8286AD5D000760AB0 /* ActivityRowView.swift in Sources */, + 575812C22A380B5E00425BAF /* InstallerVolume.swift in Sources */, 39CF56332862B7A2006FB5D2 /* PackageCreator.swift in Sources */, 398734CE28603D7F00B4C357 /* Chunk.swift in Sources */, 3935F4A2286ACD4D00760AB0 /* InstallerExportViewItem.swift in Sources */, 393F35C228641E1F005B7165 /* HeaderView.swift in Sources */, - 5795700D2A31B081004C7051 /* CapsuleButtonStyle.swift in Sources */, + 5795700D2A31B081004C7051 /* MistActionButtonStyle.swift in Sources */, 3935F4CD286C6A5D00760AB0 /* ProcessKiller.swift in Sources */, 390451BF2856E34700E0B563 /* String+Extension.swift in Sources */, 39CF56242861CA85006FB5D2 /* DiskImageUnmounter.swift in Sources */, @@ -788,29 +791,28 @@ 398734D228603DE700B4C357 /* Array+Extension.swift in Sources */, 39FF05F82859851800A86670 /* SettingsApplicationsView.swift in Sources */, 39CF561D2861C3F5006FB5D2 /* DiskImageCreator.swift in Sources */, + 575812BA2A373A4F00425BAF /* FirmwareAlertType.swift in Sources */, 39CF55AD28615530006FB5D2 /* SettingsGeneralHelperView.swift in Sources */, 39252A7B285AC50400956C74 /* SettingsDiskImagesView.swift in Sources */, 39252A79285A85AF00956C74 /* SettingsInstallersView.swift in Sources */, 39252A9F285C140D00956C74 /* ShellExecutor.swift in Sources */, 39CF561A2861C2D1006FB5D2 /* DirectoryCreator.swift in Sources */, 39252A77285A849F00956C74 /* AppDelegate.swift in Sources */, - 3935F49F286AC32C00760AB0 /* ListRow.swift in Sources */, 3935F47C2864434B00760AB0 /* SettingsGeneralNotificationsView.swift in Sources */, 3935F4C7286B54E200760AB0 /* SparkleUpdater.swift in Sources */, 393F35BE2864197F005B7165 /* PrivilegedHelperTool.swift in Sources */, 573A23622A28711C00EC9470 /* Architecture.swift in Sources */, 390451B92856E24200E0B563 /* Firmware.swift in Sources */, 390451CE2856F42800E0B563 /* DownloadType.swift in Sources */, - 3935F4CB286C1EC500760AB0 /* DownloadView.swift in Sources */, + 3935F4CB286C1EC500760AB0 /* ActivityView.swift in Sources */, 398734C6286011C300B4C357 /* Validator.swift in Sources */, 39252ABB285C7D3800956C74 /* SettingsInstallersCatalogsView.swift in Sources */, 393D8029286EB4D6008AA8E3 /* EmptyCollectionView.swift in Sources */, 3935F490286976D000760AB0 /* ProgressAlertType.swift in Sources */, - 575812B72A372D7200425BAF /* CapsuleButtonStyleType.swift in Sources */, 39FF05F62859850F00A86670 /* SettingsFirmwaresView.swift in Sources */, - 3935F4A6286AD3E100760AB0 /* DownloadHeaderView.swift in Sources */, + 3935F4A6286AD3E100760AB0 /* ActivityHeaderView.swift in Sources */, 3935F480286551FB00760AB0 /* Double+Extension.swift in Sources */, - 39252ABD285C8FFC00956C74 /* InstallerListRow.swift in Sources */, + 39252ABD285C8FFC00956C74 /* ListRowInstaller.swift in Sources */, 3935F49D286ABE4D00760AB0 /* FooterView.swift in Sources */, 390451CC2856F23100E0B563 /* ScaledSystemImage.swift in Sources */, 390451DA28573ADC00E0B563 /* ExportListType.swift in Sources */, @@ -833,27 +835,28 @@ 3935F47628643AF000760AB0 /* UNNotificationAction+Extension.swift in Sources */, 39252AB3285C5D7700956C74 /* SettingsGeneralUpdatesView.swift in Sources */, 39CA25E32941D8BB0030711E /* FileAttributesUpdater.swift in Sources */, + 575812C42A3821A900425BAF /* InstallerVolumeSelectionInformationView.swift in Sources */, 3935F4AB286B04BC00760AB0 /* HelperToolInfoPropertyList.swift in Sources */, + 575812BE2A3743E300425BAF /* InstallerSheetType.swift in Sources */, 393F35BC28641181005B7165 /* RefreshState.swift in Sources */, 390451CA2856F1D300E0B563 /* ScaledImage.swift in Sources */, 39252A95285BF83D00956C74 /* MistTask.swift in Sources */, 39CF56272861E10F006FB5D2 /* Codesigner.swift in Sources */, 39148CFC28DD55B300011FF5 /* PathControl.swift in Sources */, - 3935F4892866C68000760AB0 /* DownloadSectionHeaderView.swift in Sources */, + 3935F4892866C68000760AB0 /* ActivitySectionHeaderView.swift in Sources */, 39252AB5285C706000956C74 /* URL+Extension.swift in Sources */, - 57CF961C2A34B9E0008D3B1C /* CapsuleTrailing.swift in Sources */, 390451D828573A2500E0B563 /* ExportListView.swift in Sources */, 39FF05EE2859820900A86670 /* AppCommands.swift in Sources */, 39252AA3285C3CC400956C74 /* CodesigningPickerView.swift in Sources */, 39252AA5285C463A00956C74 /* DynamicTextView.swift in Sources */, + 575812C02A37493F00425BAF /* InstallerVolumeSelectionView.swift in Sources */, 390451C22856E3F500E0B563 /* Hardware.swift in Sources */, 39CF56092861AE7F006FB5D2 /* HelperToolCommandRequest.swift in Sources */, - 57CF961A2A34B65C008D3B1C /* CapsuleLeading.swift in Sources */, - 390451C82856E94900E0B563 /* FirmwareListRow.swift in Sources */, + 390451C82856E94900E0B563 /* ListRowFirmware.swift in Sources */, 390451E528574F0000E0B563 /* CatalogType.swift in Sources */, 3935F4852866B64900760AB0 /* MistTaskSection.swift in Sources */, 390451AC2856E1D900E0B563 /* ContentView.swift in Sources */, - 3935F4A4286AD21000760AB0 /* DownloadProgressView.swift in Sources */, + 3935F4A4286AD21000760AB0 /* ActivityProgressView.swift in Sources */, 39252A89285AD0AB00956C74 /* SettingsHeaderView.swift in Sources */, 39252A85285ACDC800956C74 /* ResetToDefaultButton.swift in Sources */, 39CF560F2861B857006FB5D2 /* XPCRoute+Extension.swift in Sources */, @@ -862,6 +865,7 @@ 39CF562F2862A797006FB5D2 /* ISOConverter.swift in Sources */, 39FF05FA285985DD00A86670 /* SettingsAboutView.swift in Sources */, 3935F4AC286B04BC00760AB0 /* HelperToolLaunchdPropertyList.swift in Sources */, + 575812BC2A37406300425BAF /* ListRowDetail.swift in Sources */, 39FF05F42859850500A86670 /* SettingsGeneralView.swift in Sources */, 390451AA2856E1D900E0B563 /* MistApp.swift in Sources */, 39252AA9285C4C9000956C74 /* RefreshRowView.swift in Sources */, @@ -871,7 +875,7 @@ 390451E1285740E800E0B563 /* Sequence+Extension.swift in Sources */, 398734D4286046B000B4C357 /* UInt32+Extension.swift in Sources */, 390451D42856F74B00E0B563 /* Package.swift in Sources */, - 395DCD16287FE36E00C411CE /* DownloadAlertType.swift in Sources */, + 395DCD16287FE36E00C411CE /* InstallerAlertType.swift in Sources */, 39CF560028619147006FB5D2 /* HelperToolCommandType.swift in Sources */, 39252A87285ACE9C00956C74 /* FooterText.swift in Sources */, 573A23642A28791F00EC9470 /* Scene+Extension.swift in Sources */, diff --git a/Mist/AppCommands.swift b/Mist/AppCommands.swift index dcdcf22..94752a3 100644 --- a/Mist/AppCommands.swift +++ b/Mist/AppCommands.swift @@ -13,7 +13,7 @@ struct AppCommands: Commands { var openURL: OpenURLAction @ObservedObject var sparkleUpdater: SparkleUpdater @Binding var refreshing: Bool - @Binding var downloadInProgress: Bool + @Binding var tasksInProgress: Bool @CommandsBuilder var body: some Commands { CommandGroup(after: .appInfo) { @@ -27,13 +27,13 @@ struct AppCommands: Commands { refresh() } .keyboardShortcut("r") - .disabled(refreshing || downloadInProgress) + .disabled(refreshing || tasksInProgress) } CommandGroup(replacing: .systemServices) { Button("Install Privileged Helper Tool...") { install() } - .disabled(downloadInProgress) + .disabled(tasksInProgress) } CommandGroup(replacing: .help) { Button("Mist Help") { diff --git a/Mist/Assets.xcassets/Bootable Installer.imageset/Bootable Installer.png b/Mist/Assets.xcassets/Bootable Installer.imageset/Bootable Installer.png new file mode 100644 index 0000000..e82d859 Binary files /dev/null and b/Mist/Assets.xcassets/Bootable Installer.imageset/Bootable Installer.png differ diff --git a/Mist/Assets.xcassets/Bootable Installer.imageset/Contents.json b/Mist/Assets.xcassets/Bootable Installer.imageset/Contents.json new file mode 100644 index 0000000..281f99f --- /dev/null +++ b/Mist/Assets.xcassets/Bootable Installer.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Bootable Installer.png", + "idiom" : "mac" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mist/Extensions/ButtonStyle+Extension.swift b/Mist/Extensions/ButtonStyle+Extension.swift index 9f86ac3..ca33ce5 100644 --- a/Mist/Extensions/ButtonStyle+Extension.swift +++ b/Mist/Extensions/ButtonStyle+Extension.swift @@ -7,9 +7,9 @@ import SwiftUI -extension ButtonStyle where Self == CapsuleButtonStyle { +extension ButtonStyle where Self == MistActionButtonStyle { - static func capsule(_ type: CapsuleButtonStyleType) -> Self { - .init(type: type) + static var mistAction: Self { + .init() } } diff --git a/Mist/Helpers/InstallMediaCreator.swift b/Mist/Helpers/InstallMediaCreator.swift index 7f20f20..203700e 100644 --- a/Mist/Helpers/InstallMediaCreator.swift +++ b/Mist/Helpers/InstallMediaCreator.swift @@ -14,12 +14,19 @@ struct InstallMediaCreator { /// Create the macOS Install Media at the specified mount point. /// /// - Parameters: - /// - url: The URL of the `createinstallmedia` binary to execute. - /// - mountPoint: The URL of the mount point (target volume). + /// - url: The URL of the `createinstallmedia` binary to execute. + /// - mountPoint: The URL of the mount point (target volume). + /// - sierraOrOlder: `true` if the installer is macOS Sierra or older, otherwise `false`. /// /// - Throws: An `Error` if the command failed to execute. - static func create(_ url: URL, mountPoint: URL) async throws { - let arguments: [String] = [url.path, "--volume", mountPoint.path, "--nointeraction"] + static func create(_ url: URL, mountPoint: URL, sierraOrOlder: Bool) async throws { + var arguments: [String] = [url.path, "--volume", mountPoint.path, "--nointeraction"] + + if sierraOrOlder { + let applicationPath: String = url.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent().path + arguments += ["--applicationpath", applicationPath] + } + let client: XPCClient = XPCClient.forMachService(named: .helperIdentifier) let request: HelperToolCommandRequest = HelperToolCommandRequest(type: .createinstallmedia, arguments: arguments, environment: [:]) let response: HelperToolCommandResponse = try await client.sendMessage(request, to: XPCRoute.commandRoute) diff --git a/Mist/Helpers/TaskManager.swift b/Mist/Helpers/TaskManager.swift index 9ea6c27..36232c4 100644 --- a/Mist/Helpers/TaskManager.swift +++ b/Mist/Helpers/TaskManager.swift @@ -226,6 +226,44 @@ class TaskManager: ObservableObject { return taskGroups } + // swiftlint:disable:next function_parameter_count + static func taskGroups( + for installer: Installer, + cacheDownloads: Bool, + cacheDirectory: String, + retries: Int, + delay retryDelay: Int, + volume: InstallerVolume + ) throws -> [(section: MistTaskSection, tasks: [MistTask])] { + let cacheDirectoryURL: URL = URL(fileURLWithPath: cacheDirectory).appendingPathComponent(installer.id) + let temporaryDirectoryURL: URL = URL(fileURLWithPath: .temporaryDirectory) + let taskGroups: [(section: MistTaskSection, tasks: [MistTask])] = [ + ( + section: .download, + tasks: try downloadTasks(for: installer, cacheDirectory: cacheDirectoryURL, retries: retries, delay: retryDelay) + ), + ( + section: .setup, + tasks: installTasks(for: installer, temporaryDirectory: temporaryDirectoryURL, mountPoint: installer.temporaryDiskImageMountPointURL, cacheDirectory: cacheDirectory) + ), + ( + section: .bootableInstaller, + tasks: bootableInstallerTasks(for: installer, volume: volume) + ), + ( + section: .cleanup, + tasks: cleanupTasks( + mountPoint: installer.temporaryDiskImageMountPointURL, + temporaryDirectory: temporaryDirectoryURL, + cacheDownloads: cacheDownloads, + cacheDirectory: cacheDirectoryURL + ) + ) + ] + + return taskGroups + } + private static func downloadTasks(for installer: Installer, cacheDirectory cacheDirectoryURL: URL, retries: Int, delay retryDelay: Int) throws -> [MistTask] { var tasks: [MistTask] = [] @@ -403,7 +441,7 @@ class TaskManager: ObservableObject { try await DiskImageMounter.mount(temporaryImageURL, mountPoint: installer.temporaryISOMountPointURL) }, MistTask(type: .create, description: "macOS Installer in temporary Disk Image") { - try await InstallMediaCreator.create(createInstallMediaURL, mountPoint: installer.temporaryISOMountPointURL) + try await InstallMediaCreator.create(createInstallMediaURL, mountPoint: installer.temporaryISOMountPointURL, sierraOrOlder: installer.sierraOrOlder) }, MistTask(type: .unmount, description: "temporary Disk Image") { if FileManager.default.fileExists(atPath: installer.temporaryISOMountPointURL.path) { @@ -470,6 +508,18 @@ class TaskManager: ObservableObject { return tasks } + private static func bootableInstallerTasks(for installer: Installer, volume: InstallerVolume) -> [MistTask] { + let createInstallMediaURL: URL = installer.temporaryInstallerURL.appendingPathComponent("/Contents/Resources/createinstallmedia") + let mountPointURL: URL = URL(fileURLWithPath: volume.path) + let tasks: [MistTask] = [ + MistTask(type: .create, description: "Bootable Installer") { + try await InstallMediaCreator.create(createInstallMediaURL, mountPoint: mountPointURL, sierraOrOlder: installer.sierraOrOlder) + } + ] + + return tasks + } + private static func cleanupTasks(mountPoint mountPointURL: URL, temporaryDirectory temporaryDirectoryURL: URL, cacheDownloads: Bool, cacheDirectory cacheDirectoryURL: URL) -> [MistTask] { var tasks: [MistTask] = [ diff --git a/Mist/MistApp.swift b/Mist/MistApp.swift index edacd10..68c1b40 100644 --- a/Mist/MistApp.swift +++ b/Mist/MistApp.swift @@ -14,18 +14,18 @@ struct MistApp: App { var appDelegate: AppDelegate @StateObject var sparkleUpdater: SparkleUpdater = SparkleUpdater() @State private var refreshing: Bool = false - @State private var downloadInProgress: Bool = false + @State private var tasksInProgress: Bool = false var body: some Scene { WindowGroup { - ContentView(refreshing: $refreshing, downloadInProgress: $downloadInProgress) + ContentView(refreshing: $refreshing, tasksInProgress: $tasksInProgress) .onReceive(NotificationCenter.default.publisher(for: NSApplication.willUpdateNotification)) { _ in hideZoomButton() } } .fixedWindow() .commands { - AppCommands(sparkleUpdater: sparkleUpdater, refreshing: $refreshing, downloadInProgress: $downloadInProgress) + AppCommands(sparkleUpdater: sparkleUpdater, refreshing: $refreshing, tasksInProgress: $tasksInProgress) } Settings { SettingsView(sparkleUpdater: sparkleUpdater) diff --git a/Mist/Model/Firmware.swift b/Mist/Model/Firmware.swift index 5a98a7b..acbdcf8 100644 --- a/Mist/Model/Firmware.swift +++ b/Mist/Model/Firmware.swift @@ -97,6 +97,14 @@ struct Firmware: Decodable, Hashable, Identifiable { "beta": beta ] } + var tooltip: String { + """ + Version: \(version) + Build Number: \(build) + Release Date: \(formattedDate) + Download Size: \(size.bytesString()) + """ + } /// Perform a lookup and retrieve a list of supported Firmware builds for this Mac. /// diff --git a/Mist/Model/FirmwareAlertType.swift b/Mist/Model/FirmwareAlertType.swift new file mode 100644 index 0000000..2dff882 --- /dev/null +++ b/Mist/Model/FirmwareAlertType.swift @@ -0,0 +1,13 @@ +// +// FirmwareAlertType.swift +// Mist +// +// Created by Nindi Gill on 12/6/2023. +// + +import SwiftUI + +enum FirmwareAlertType: String { + case compatibility = "Compatiblity" + case helperTool = "Helper Tool" +} diff --git a/Mist/Model/Installer.swift b/Mist/Model/Installer.swift index 2e1fd7c..7fa4517 100644 --- a/Mist/Model/Installer.swift +++ b/Mist/Model/Installer.swift @@ -751,6 +751,9 @@ struct Installer: Decodable, Hashable, Identifiable { "beta": beta ] } + var mavericksOrNewer: Bool { + bigSurOrNewer || version.range(of: "^10\\.(9|1[0-5])\\.", options: .regularExpression) != nil + } var sierraOrOlder: Bool { version.range(of: "^10\\.([7-9]|1[0-2])\\.", options: .regularExpression) != nil } @@ -775,6 +778,14 @@ struct Installer: Decodable, Hashable, Identifiable { var isoSize: Double { ceil(Double(size) / Double(UInt64.gigabyte)) + 1.5 } + var tooltip: String { + """ + Version: \(version) + Build Number: \(build) + Release Date: \(date) + Download Size: \(size.bytesString()) + """ + } } extension Installer: Equatable { diff --git a/Mist/Model/DownloadAlertType.swift b/Mist/Model/InstallerAlertType.swift similarity index 78% rename from Mist/Model/DownloadAlertType.swift rename to Mist/Model/InstallerAlertType.swift index bbe9b53..61381a2 100644 --- a/Mist/Model/DownloadAlertType.swift +++ b/Mist/Model/InstallerAlertType.swift @@ -1,11 +1,11 @@ // -// DownloadAlertType.swift +// InstallerAlertType.swift // Mist // // Created by Nindi Gill on 14/7/2022. // -enum DownloadAlertType: String { +enum InstallerAlertType: String { case compatibility = "Compatiblity" case helperTool = "Helper Tool" case fullDiskAccess = "Full Disk Access" diff --git a/Mist/Model/InstallerSheetType.swift b/Mist/Model/InstallerSheetType.swift new file mode 100644 index 0000000..0ece950 --- /dev/null +++ b/Mist/Model/InstallerSheetType.swift @@ -0,0 +1,14 @@ +// +// InstallerSheetType.swift +// Mist +// +// Created by Nindi Gill on 12/6/2023. +// + +import Foundation + +enum InstallerSheetType: String { + case download = "Download" + case volumeSelection = "Volume Selection" + case createBootableInstaller = "Create Bootable Installer" +} diff --git a/Mist/Model/InstallerVolume.swift b/Mist/Model/InstallerVolume.swift new file mode 100644 index 0000000..f5f4d2a --- /dev/null +++ b/Mist/Model/InstallerVolume.swift @@ -0,0 +1,18 @@ +// +// InstallerVolume.swift +// Mist +// +// Created by Nindi Gill on 13/6/2023. +// + +import Foundation + +struct InstallerVolume: Identifiable, Hashable { + static let placeholder: InstallerVolume = InstallerVolume(id: "placeholder", name: "No volume selected", path: "", capacity: 0) + static let invalid: InstallerVolume = InstallerVolume(id: "invalid", name: "No available volumes found", path: "", capacity: 0) + + var id: String + var name: String + var path: String + var capacity: UInt64 +} diff --git a/Mist/Model/MistTaskSection.swift b/Mist/Model/MistTaskSection.swift index 219872a..6b4d9f2 100644 --- a/Mist/Model/MistTaskSection.swift +++ b/Mist/Model/MistTaskSection.swift @@ -12,6 +12,7 @@ enum MistTaskSection: String, CaseIterable, Identifiable { case diskImage = "Disk Image" case iso = "ISO" case package = "Package" + case bootableInstaller = "Bootable Installer" case cleanup = "Cleanup" var id: String { diff --git a/Mist/Views/Download/DownloadHeaderView.swift b/Mist/Views/Activity/ActivityHeaderView.swift similarity index 79% rename from Mist/Views/Download/DownloadHeaderView.swift rename to Mist/Views/Activity/ActivityHeaderView.swift index 6782282..1cfd898 100644 --- a/Mist/Views/Download/DownloadHeaderView.swift +++ b/Mist/Views/Activity/ActivityHeaderView.swift @@ -1,5 +1,5 @@ // -// DownloadHeaderView.swift +// ActivityHeaderView.swift // Mist // // Created by Nindi Gill on 28/6/2022. @@ -7,7 +7,7 @@ import SwiftUI -struct DownloadHeaderView: View { +struct ActivityHeaderView: View { var imageName: String var name: String var version: String @@ -31,12 +31,12 @@ struct DownloadHeaderView: View { } } -struct DownloadHeaderView_Previews: PreviewProvider { +struct ActivityHeaderView_Previews: PreviewProvider { static let firmware: Firmware = .example static let installer: Installer = .example static var previews: some View { - DownloadHeaderView(imageName: firmware.imageName, name: firmware.name, version: firmware.version, build: firmware.build, beta: false) - DownloadHeaderView(imageName: installer.imageName, name: installer.name, version: installer.version, build: installer.build, beta: false) + ActivityHeaderView(imageName: firmware.imageName, name: firmware.name, version: firmware.version, build: firmware.build, beta: false) + ActivityHeaderView(imageName: installer.imageName, name: installer.name, version: installer.version, build: installer.build, beta: false) } } diff --git a/Mist/Views/Download/DownloadProgressView.swift b/Mist/Views/Activity/ActivityProgressView.swift similarity index 81% rename from Mist/Views/Download/DownloadProgressView.swift rename to Mist/Views/Activity/ActivityProgressView.swift index 90a6490..eb62ed4 100644 --- a/Mist/Views/Download/DownloadProgressView.swift +++ b/Mist/Views/Activity/ActivityProgressView.swift @@ -1,5 +1,5 @@ // -// DownloadProgressView.swift +// ActivityProgressView.swift // Mist // // Created by Nindi Gill on 28/6/2022. @@ -7,7 +7,7 @@ import SwiftUI -struct DownloadProgressView: View { +struct ActivityProgressView: View { var state: MistTaskState var value: CGFloat var size: UInt64 @@ -36,9 +36,9 @@ struct DownloadProgressView: View { } } -struct DownloadProgressView_Previews: PreviewProvider { +struct ActivityProgressView_Previews: PreviewProvider { static var previews: some View { - DownloadProgressView(state: .inProgress, value: 0.5, size: Firmware.example.size) - DownloadProgressView(state: .inProgress, value: 0.5, size: Installer.example.size) + ActivityProgressView(state: .inProgress, value: 0.5, size: Firmware.example.size) + ActivityProgressView(state: .inProgress, value: 0.5, size: Installer.example.size) } } diff --git a/Mist/Views/Download/DownloadRowView.swift b/Mist/Views/Activity/ActivityRowView.swift similarity index 84% rename from Mist/Views/Download/DownloadRowView.swift rename to Mist/Views/Activity/ActivityRowView.swift index fee1ce7..28dbeb6 100644 --- a/Mist/Views/Download/DownloadRowView.swift +++ b/Mist/Views/Activity/ActivityRowView.swift @@ -1,5 +1,5 @@ // -// DownloadRowView.swift +// ActivityRowView.swift // Mist // // Created by Nindi Gill on 28/6/2022. @@ -7,7 +7,7 @@ import SwiftUI -struct DownloadRowView: View { +struct ActivityRowView: View { var state: MistTaskState var description: String var degrees: CGFloat @@ -30,8 +30,8 @@ struct DownloadRowView: View { } } -struct DownloadRowView_Previews: PreviewProvider { +struct ActivityRowView_Previews: PreviewProvider { static var previews: some View { - DownloadRowView(state: .inProgress, description: "Downloading...", degrees: 360) + ActivityRowView(state: .inProgress, description: "Downloading...", degrees: 360) } } diff --git a/Mist/Views/Download/DownloadSectionHeaderView.swift b/Mist/Views/Activity/ActivitySectionHeaderView.swift similarity index 73% rename from Mist/Views/Download/DownloadSectionHeaderView.swift rename to Mist/Views/Activity/ActivitySectionHeaderView.swift index f93ecb1..649ff71 100644 --- a/Mist/Views/Download/DownloadSectionHeaderView.swift +++ b/Mist/Views/Activity/ActivitySectionHeaderView.swift @@ -1,5 +1,5 @@ // -// DownloadSectionHeaderView.swift +// ActivitySectionHeaderView.swift // Mist // // Created by Nindi Gill on 25/6/2022. @@ -7,7 +7,7 @@ import SwiftUI -struct DownloadSectionHeaderView: View { +struct ActivitySectionHeaderView: View { var section: MistTaskSection private let length: CGFloat = 24 @@ -23,10 +23,10 @@ struct DownloadSectionHeaderView: View { } } -struct DownloadSectionHeaderView_Previews: PreviewProvider { +struct ActivitySectionHeaderView_Previews: PreviewProvider { static var previews: some View { ForEach(MistTaskSection.allCases) { section in - DownloadSectionHeaderView(section: section) + ActivitySectionHeaderView(section: section) } } } diff --git a/Mist/Views/Download/DownloadView.swift b/Mist/Views/Activity/ActivityView.swift similarity index 87% rename from Mist/Views/Download/DownloadView.swift rename to Mist/Views/Activity/ActivityView.swift index fb9f0af..ef14a04 100644 --- a/Mist/Views/Download/DownloadView.swift +++ b/Mist/Views/Activity/ActivityView.swift @@ -1,5 +1,5 @@ // -// DownloadView.swift +// ActivityView.swift // Mist // // Created by Nindi Gill on 29/6/2022. @@ -8,7 +8,7 @@ import Combine import SwiftUI -struct DownloadView: View { +struct ActivityView: View { // swiftlint:disable:next weak_delegate @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate: AppDelegate @@ -35,6 +35,12 @@ struct DownloadView: View { @State private var timer: Publishers.Autoconnect = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect() private let width: CGFloat = 420 private let height: CGFloat = 640 + private var bootableInstaller: Bool { + taskManager.taskGroups.map { $0.section }.contains(.bootableInstaller) + } + private var venturaOrOlder: Bool { + !ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 14, minorVersion: 0, patchVersion: 0)) + } private var buttonText: String { switch taskManager.currentState { case .pending, .inProgress: @@ -47,20 +53,20 @@ struct DownloadView: View { var body: some View { // swiftlint:disable:next closure_body_length VStack(spacing: 0) { - DownloadHeaderView(imageName: imageName, name: name, version: version, build: build, beta: beta) + ActivityHeaderView(imageName: imageName, name: name, version: version, build: build, beta: beta) Divider() ScrollViewReader { proxy in List { ForEach(taskManager.taskGroups, id: \.section) { taskGroup in - Section(header: DownloadSectionHeaderView(section: taskGroup.section)) { + Section(header: ActivitySectionHeaderView(section: taskGroup.section)) { ForEach(taskGroup.tasks.indices, id: \.self) { index in VStack { - DownloadRowView(state: taskGroup.tasks[index].state, description: taskGroup.tasks[index].currentDescription, degrees: degrees) + ActivityRowView(state: taskGroup.tasks[index].state, description: taskGroup.tasks[index].currentDescription, degrees: degrees) if taskGroup.tasks[index].type == .download && taskGroup.tasks[index].state != .pending, let size: UInt64 = taskGroup.tasks[index].downloadSize { - DownloadProgressView(state: taskGroup.tasks[index].state, value: value, size: size) + ActivityProgressView(state: taskGroup.tasks[index].state, value: value, size: size) } - if index < taskGroup.tasks.count - 1 { + if venturaOrOlder && index != taskGroup.tasks.count { Divider() } } @@ -70,7 +76,7 @@ struct DownloadView: View { } } .onChange(of: currentTaskId) { id in - withAnimation(.easeOut(duration: 1.0)) { + withAnimation(.easeOut(duration: 1)) { proxy.scrollTo(id, anchor: .center) } } @@ -157,6 +163,7 @@ struct DownloadView: View { } if showInFinder { + guard let url: URL = destinationURL else { return } @@ -192,7 +199,14 @@ struct DownloadView: View { } private func sendNotification(for type: DownloadType, name: String, version: String, build: String, success: Bool) { - let title: String = " \(type.description) download\(success ? "ed" : " failed")" + let title: String + + if bootableInstaller { + title = "Bootable Installer \(success ? "created" : "failed")" + } else { + title = "\(type.description) \(success ? "downloaded" : "failed")" + } + let body: String = "\(name) \(version) (\(build))" appDelegate.sendUpdateNotification(title: title, body: body, success: success, url: destinationURL) } @@ -218,12 +232,12 @@ struct DownloadView: View { } } -struct DownloadView_Previews: PreviewProvider { +struct ActivityView_Previews: PreviewProvider { static let firmware: Firmware = .example static let installer: Installer = .example static var previews: some View { - DownloadView(downloadType: .firmware, imageName: firmware.imageName, name: firmware.name, version: firmware.version, build: firmware.build, beta: false, taskManager: .shared) - DownloadView(downloadType: .installer, imageName: installer.imageName, name: installer.name, version: installer.version, build: installer.build, beta: false, taskManager: .shared) + ActivityView(downloadType: .firmware, imageName: firmware.imageName, name: firmware.name, version: firmware.version, build: firmware.build, beta: false, taskManager: .shared) + ActivityView(downloadType: .installer, imageName: installer.imageName, name: installer.name, version: installer.version, build: installer.build, beta: false, taskManager: .shared) } } diff --git a/Mist/Views/Components/Capsule/CapsuleButtonStyleType.swift b/Mist/Views/Components/Capsule/CapsuleButtonStyleType.swift deleted file mode 100644 index e1dd6fb..0000000 --- a/Mist/Views/Components/Capsule/CapsuleButtonStyleType.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// CapsuleButtonStyleType.swift -// Mist -// -// Created by Nindi Gill on 12/6/2023. -// - -import SwiftUI - -/// Capsule Button Style Type -enum CapsuleButtonStyleType { - /// Standard capsule with both leading and trailing edges curved - case standard - /// Capsule with leading edge curved only - case leading - /// Capsule with trailing edge curved only - case trailing -} diff --git a/Mist/Views/Components/Capsule/CapsuleLeading.swift b/Mist/Views/Components/Capsule/CapsuleLeading.swift deleted file mode 100644 index fcaf820..0000000 --- a/Mist/Views/Components/Capsule/CapsuleLeading.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// CapsuleLeading.swift -// Mist -// -// Created by Nindi Gill on 10/6/2023. -// - -import SwiftUI - -struct CapsuleLeading: Shape { - - func path(in rect: CGRect) -> Path { - var path: Path = Path() - path.move(to: CGPoint(x: rect.maxX, y: rect.minY)) - path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) - path.addLine(to: CGPoint(x: rect.height / 2, y: rect.maxY)) - path.addArc( - center: CGPoint(x: rect.height / 2, y: rect.midY), - radius: rect.height / 2, - startAngle: .degrees(270), - endAngle: .degrees(90), - clockwise: true - ) - path.addLine(to: CGPoint(x: rect.height / 2, y: rect.minY)) - path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) - path.closeSubpath() - return path - } -} diff --git a/Mist/Views/Components/Capsule/CapsuleTrailing.swift b/Mist/Views/Components/Capsule/CapsuleTrailing.swift deleted file mode 100644 index 465108c..0000000 --- a/Mist/Views/Components/Capsule/CapsuleTrailing.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// CapsuleTrailing.swift -// Mist -// -// Created by Nindi Gill on 11/6/2023. -// - -import SwiftUI - -struct CapsuleTrailing: Shape { - - func path(in rect: CGRect) -> Path { - var path: Path = Path() - path.move(to: CGPoint(x: rect.minX, y: rect.minY)) - path.addLine(to: CGPoint(x: rect.maxX - rect.height / 2, y: rect.minY)) - path.addArc( - center: CGPoint(x: rect.maxX - rect.height / 2, y: rect.midY), - radius: rect.height / 2, - startAngle: .degrees(90), - endAngle: .degrees(270), - clockwise: true - ) - path.addLine(to: CGPoint(x: rect.maxX - rect.height / 2, y: rect.maxY)) - path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) - path.addLine(to: CGPoint(x: rect.minX, y: rect.minY)) - path.closeSubpath() - return path - } -} diff --git a/Mist/Views/Components/Capsule/CapsuleButtonStyle.swift b/Mist/Views/Components/MistActionButtonStyle.swift similarity index 50% rename from Mist/Views/Components/Capsule/CapsuleButtonStyle.swift rename to Mist/Views/Components/MistActionButtonStyle.swift index 693a5ef..feaa26b 100644 --- a/Mist/Views/Components/Capsule/CapsuleButtonStyle.swift +++ b/Mist/Views/Components/MistActionButtonStyle.swift @@ -1,5 +1,5 @@ // -// CapsuleButtonStyle.swift +// MistActionButtonStyle.swift // Mist // // Created by Nindi Gill on 5/6/2023. @@ -7,28 +7,15 @@ import SwiftUI -struct CapsuleButtonStyle: ButtonStyle { - - let type: CapsuleButtonStyleType +struct MistActionButtonStyle: ButtonStyle { private let padding: CGFloat = 5 - @ViewBuilder func makeBody(configuration: Configuration) -> some View { - - let view: some View = configuration.label + configuration.label .font(.body.bold()) .padding(.horizontal) .padding(.vertical, padding) .foregroundColor(.white) .background(Color.accentColor.brightness(configuration.isPressed ? -0.5 : 0)) - - switch type { - case .standard: - view.clipShape(Capsule()) - case .leading: - view.clipShape(CapsuleLeading()) - case .trailing: - view.clipShape(CapsuleTrailing()) - } } } diff --git a/Mist/Views/ContentView.swift b/Mist/Views/ContentView.swift index 5177bc1..334ed6e 100644 --- a/Mist/Views/ContentView.swift +++ b/Mist/Views/ContentView.swift @@ -15,7 +15,7 @@ struct ContentView: View { @AppStorage("showCompatible") private var showCompatible: Bool = false @Binding var refreshing: Bool - @Binding var downloadInProgress: Bool + @Binding var tasksInProgress: Bool @State private var firmwares: [Firmware] = [] @State private var installers: [Installer] = [] @State private var searchString: String = "" @@ -45,7 +45,6 @@ struct ContentView: View { return filteredFirmwares } - private var filteredInstallers: [Installer] { var filteredInstallers: [Installer] = installers @@ -85,12 +84,12 @@ struct ContentView: View { switch downloadType { case .firmware: ForEach(filteredFirmwares(for: releaseName)) { firmware in - FirmwareListRow(firmware: firmware, savePanel: $savePanel, downloadInProgress: $downloadInProgress, taskManager: taskManager) + ListRowFirmware(firmware: firmware, savePanel: $savePanel, tasksInProgress: $tasksInProgress, taskManager: taskManager) .tag(firmware) } case .installer: ForEach(filteredInstallers(for: releaseName)) { installer in - InstallerListRow(installer: installer, openPanel: $openPanel, downloadInProgress: $downloadInProgress, taskManager: taskManager) + ListRowInstaller(installer: installer, openPanel: $openPanel, tasksInProgress: $tasksInProgress, taskManager: taskManager) .tag(installer) } } @@ -165,6 +164,6 @@ struct ContentView: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView(refreshing: .constant(false), downloadInProgress: .constant(false)) + ContentView(refreshing: .constant(false), tasksInProgress: .constant(false)) } } diff --git a/Mist/Views/List/FirmwareListRow.swift b/Mist/Views/List/FirmwareListRow.swift deleted file mode 100644 index 0a3919e..0000000 --- a/Mist/Views/List/FirmwareListRow.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// FirmwareListRow.swift -// Mist -// -// Created by Nindi Gill on 13/6/2022. -// - -import SwiftUI - -struct FirmwareListRow: View { - @AppStorage("firmwareFilename") - private var firmwareFilename: String = .firmwareFilenameTemplate - @AppStorage("retries") - private var retries: Int = 10 - @AppStorage("retryDelay") - private var retryDelay: Int = 30 - var firmware: Firmware - @Binding var savePanel: NSSavePanel - @Binding var downloadInProgress: Bool - @ObservedObject var taskManager: TaskManager - @State private var showSavePanel: Bool = false - @State private var downloading: Bool = false - - var body: some View { - ListRow( - type: .firmware, - image: firmware.imageName, - version: firmware.version, - build: firmware.build, - beta: firmware.beta, - date: firmware.formattedDate, - size: firmware.size.bytesString(), - compatible: firmware.compatible, - showPanel: $showSavePanel, - taskManager: taskManager - ) - .onChange(of: showSavePanel) { boolean in - - if boolean { - save() - } - } - .sheet(isPresented: $downloading) { - DownloadView( - downloadType: .firmware, - imageName: firmware.imageName, - name: firmware.name, - version: firmware.version, - build: firmware.build, - beta: firmware.beta, - destinationURL: savePanel.url, - taskManager: taskManager - ) - } - } - - private func save() { - showSavePanel = false - savePanel.title = "Download Firmware" - savePanel.nameFieldStringValue = firmwareFilename.stringWithSubstitutions(name: firmware.name, version: firmware.version, build: firmware.build) - savePanel.canCreateDirectories = true - savePanel.canSelectHiddenExtension = true - savePanel.isExtensionHidden = false - - Task { - let response: NSApplication.ModalResponse = savePanel.runModal() - - guard response == .OK else { - return - } - - taskManager.taskGroups = try TaskManager.taskGroups(for: firmware, destination: savePanel.url, retries: retries, delay: retryDelay) - downloading = true - downloadInProgress = true - } - } -} - -struct FirmwareListRow_Previews: PreviewProvider { - - static var previews: some View { - FirmwareListRow(firmware: .example, savePanel: .constant(NSSavePanel()), downloadInProgress: .constant(false), taskManager: .shared) - } -} diff --git a/Mist/Views/List/InstallerExportView.swift b/Mist/Views/List/InstallerExportView.swift index f27ee68..53c8e30 100644 --- a/Mist/Views/List/InstallerExportView.swift +++ b/Mist/Views/List/InstallerExportView.swift @@ -23,7 +23,20 @@ struct InstallerExportView: View { return false } - return architecture == .intel || (architecture == .appleSilicon && installer.bigSurOrNewer) + return (architecture == .intel && installer.mavericksOrNewer) || (architecture == .appleSilicon && installer.bigSurOrNewer) + } + private var compatibilityMessage: String { + + guard let architecture: Architecture = Hardware.architecture else { + return "" + } + + switch architecture { + case .appleSilicon: + return "**Note:** ISOs are unavailable for building **macOS Catalina 10.15 and older** on [Apple Silicon Macs](https://support.apple.com/en-us/HT211814)." + case .intel: + return "**Note:** ISOs are unavailable for building **OS X Mountain Lion 10.8 and older** on Intel-based Macs." + } } var body: some View { @@ -36,16 +49,15 @@ struct InstallerExportView: View { .disabled(exports.count == 1 && exportApplication) InstallerExportViewItem(exportType: .diskImage, selected: $exportDiskImage) .disabled(exports.count == 1 && exportDiskImage) - if isoCompatible { - InstallerExportViewItem(exportType: .iso, selected: $exportISO) - .disabled(exports.count == 1 && exportISO) - } + InstallerExportViewItem(exportType: .iso, selected: $exportISO) + .disabled(isoCompatible ? exports.count == 1 && exportISO : true) + .opacity(isoCompatible ? 1 : 0.5) InstallerExportViewItem(exportType: .package, selected: $exportPackage) .disabled(exports.count == 1 && exportPackage) Spacer() } if !isoCompatible { - Text("**Note:** ISOs are unavailable for building **macOS Catalina 10.15 and older** on [Apple Silicon Macs](https://support.apple.com/en-us/HT211814).") + Text(.init(compatibilityMessage)) .padding(.top) } } diff --git a/Mist/Views/List/InstallerListRow.swift b/Mist/Views/List/InstallerListRow.swift deleted file mode 100644 index 719e0ae..0000000 --- a/Mist/Views/List/InstallerListRow.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// InstallerListRow.swift -// Mist -// -// Created by Nindi Gill on 17/6/2022. -// - -import SwiftUI - -struct InstallerListRow: View { - @AppStorage("cacheDownloads") - private var cacheDownloads: Bool = false - @AppStorage("cacheDirectory") - private var cacheDirectory: String = .cacheDirectory - @AppStorage("applicationFilename") - private var applicationFilename: String = .applicationFilenameTemplate - @AppStorage("diskImageFilename") - private var diskImageFilename: String = .diskImageFilenameTemplate - @AppStorage("diskImageSign") - private var diskImageSign: Bool = false - @AppStorage("diskImageSigningIdentity") - private var diskImageSigningIdentity: String = "" - @AppStorage("isoFilename") - private var isoFilename: String = .isoFilenameTemplate - @AppStorage("packageFilename") - private var packageFilename: String = .packageFilenameTemplate - @AppStorage("packageIdentifier") - private var packageIdentifier: String = .packageIdentifierTemplate - @AppStorage("packageSign") - private var packageSign: Bool = false - @AppStorage("packageSigningIdentity") - private var packageSigningIdentity: String = "" - @AppStorage("retries") - private var retries: Int = 10 - @AppStorage("retryDelay") - private var retryDelay: Int = 30 - var installer: Installer - @Binding var openPanel: NSOpenPanel - @Binding var downloadInProgress: Bool - @ObservedObject var taskManager: TaskManager - @State private var showOpenPanel: Bool = false - @State private var downloading: Bool = false - @State private var exports: [InstallerExportType] = [] - - var body: some View { - ListRow( - type: .installer, - image: installer.imageName, - version: installer.version, - build: installer.build, - beta: installer.beta, - date: installer.date, - size: installer.size.bytesString(), - compatible: installer.compatible, - showPanel: $showOpenPanel, - taskManager: taskManager - ) - .onChange(of: showOpenPanel) { boolean in - - if boolean { - open() - } - } - .sheet(isPresented: $downloading) { - DownloadView( - downloadType: .installer, - imageName: installer.imageName, - name: installer.name.replacingOccurrences(of: " beta", with: ""), - version: installer.version, - build: installer.build, - beta: installer.beta, - destinationURL: openPanel.url, - taskManager: taskManager - ) - } - } - - private func open() { - showOpenPanel = false - openPanel.title = "Download Installer" - openPanel.canChooseFiles = false - openPanel.canChooseDirectories = true - openPanel.allowsMultipleSelection = false - openPanel.prompt = "Save" - openPanel.accessoryView = NSHostingView(rootView: InstallerExportView(installer: installer, exports: $exports)) - openPanel.isAccessoryViewDisclosed = true - - Task { - let response: NSApplication.ModalResponse = openPanel.runModal() - - guard response == .OK else { - return - } - - taskManager.taskGroups = try TaskManager.taskGroups( - for: installer, - destination: openPanel.url, - exports: exports, - cacheDownloads: cacheDownloads, - cacheDirectory: cacheDirectory, - retries: retries, - delay: retryDelay, - applicationFilename: applicationFilename, - diskImageFilename: diskImageFilename, - diskImageSign: diskImageSign, - diskImageSigningIdentity: diskImageSigningIdentity, - isoFilename: isoFilename, - packageFilename: packageFilename, - packageIdentifier: packageIdentifier, - packageSign: packageSign, - packageSigningIdentity: packageSigningIdentity - ) - - downloading = true - downloadInProgress = true - } - } -} - -struct InstallerListRow_Previews: PreviewProvider { - static var previews: some View { - InstallerListRow(installer: .example, openPanel: .constant(NSOpenPanel()), downloadInProgress: .constant(false), taskManager: .shared) - } -} diff --git a/Mist/Views/List/InstallerVolumeSelectionInformationView.swift b/Mist/Views/List/InstallerVolumeSelectionInformationView.swift new file mode 100644 index 0000000..0cbe5d1 --- /dev/null +++ b/Mist/Views/List/InstallerVolumeSelectionInformationView.swift @@ -0,0 +1,37 @@ +// +// InstallerVolumeSelectionInformationView.swift +// Mist +// +// Created by Nindi Gill on 13/6/2023. +// + +import SwiftUI + +struct InstallerVolumeSelectionInformationView: View { + private let spacing: CGFloat = 10 + + var body: some View { + VStack(alignment: .leading, spacing: spacing) { + HStack(alignment: .top) { + Image(systemName: "info.circle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(Color.white, Color.blue) + .font(.title) + Text("Only removable volumes formatted as **Mac OS Extended (Journaled)** are available for selection. Use **Disk Utility** to format volumes as required.") + } + HStack(alignment: .top) { + Image(systemName: "exclamationmark.triangle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(Color.black, Color.yellow) + .font(.title) + Text("The selected volume will be **erased automatically**. Ensure you have backed up any necessary data before proceeding.") + } + } + } +} + +struct InstallerVolumeSelectionInformationView_Previews: PreviewProvider { + static var previews: some View { + InstallerVolumeSelectionInformationView() + } +} diff --git a/Mist/Views/List/InstallerVolumeSelectionPickerView.swift b/Mist/Views/List/InstallerVolumeSelectionPickerView.swift new file mode 100644 index 0000000..dc49f5e --- /dev/null +++ b/Mist/Views/List/InstallerVolumeSelectionPickerView.swift @@ -0,0 +1,49 @@ +// +// InstallerVolumeSelectionPickerView.swift +// Mist +// +// Created by Nindi Gill on 13/6/2023. +// + +import SwiftUI + +struct InstallerVolumeSelectionPickerView: View { + @Binding var selectedVolume: InstallerVolume + var volumes: [InstallerVolume] + var refresh: () -> Void + + var body: some View { + HStack { + Picker("Selected Volume", selection: $selectedVolume) { + if volumes.isEmpty { + Text(InstallerVolume.invalid.name) + .tag(InstallerVolume.invalid) + } else { + Text(InstallerVolume.placeholder.name) + .tag(InstallerVolume.placeholder) + Divider() + ForEach(volumes) { volume in + Text("\(volume.name) - \(volume.capacity.bytesString())") + .tag(volume) + } + } + } + .pickerStyle(.menu) + .labelsHidden() + Button { + refresh() + } label: { + Image(systemName: "arrow.clockwise") + .foregroundColor(.accentColor) + } + .help("Refresh") + } + .padding(.vertical) + } +} + +struct InstallerVolumeSelectionPickerView_Previews: PreviewProvider { + static var previews: some View { + InstallerVolumeSelectionPickerView(selectedVolume: .constant(.placeholder), volumes: []) { } + } +} diff --git a/Mist/Views/List/InstallerVolumeSelectionView.swift b/Mist/Views/List/InstallerVolumeSelectionView.swift new file mode 100644 index 0000000..ca94f3f --- /dev/null +++ b/Mist/Views/List/InstallerVolumeSelectionView.swift @@ -0,0 +1,106 @@ +// +// InstallerVolumeSelectionView.swift +// Mist +// +// Created by Nindi Gill on 12/6/2023. +// + +import SwiftUI + +struct InstallerVolumeSelectionView: View { + @Environment(\.presentationMode) + var presentationMode: Binding + @Binding var volume: InstallerVolume? + @State private var selectedVolume: InstallerVolume = .placeholder + @State private var volumes: [InstallerVolume] = [.placeholder] + private let padding: CGFloat = 5 + private let width: CGFloat = 420 + private let height: CGFloat = 320 + + var body: some View { + VStack(spacing: 0) { + Text("Create a Bootable Installer for macOS") + .font(.title2) + .padding(.vertical) + Divider() + Spacer() + VStack { + Text("Select a volume to create a bootable macOS Installer:") + InstallerVolumeSelectionPickerView(selectedVolume: $selectedVolume, volumes: volumes, refresh: refresh) + InstallerVolumeSelectionInformationView() + } + .padding(.horizontal) + .padding(.vertical, padding) + Spacer() + Divider() + HStack { + Button("Open Disk Utility") { + openDiskUtility() + } + Spacer() + Button("Select") { + volume = selectedVolume + presentationMode.wrappedValue.dismiss() + } + .buttonStyle(.borderedProminent) + .disabled([InstallerVolume.placeholder, InstallerVolume.invalid].contains(selectedVolume)) + Button("Cancel") { + presentationMode.wrappedValue.dismiss() + } + } + .padding() + } + .frame(width: width, height: height) + .onAppear { + refresh() + } + } + + private func refresh() { + volumes = getAvailableVolumes() + selectedVolume = volumes.isEmpty ? InstallerVolume.invalid : InstallerVolume.placeholder + } + + private func getAvailableVolumes() -> [InstallerVolume] { + + var volumes: [InstallerVolume] = [] + let keys: [URLResourceKey] = [.volumeNameKey, .volumeLocalizedFormatDescriptionKey, .volumeIsReadOnlyKey, .volumeTotalCapacityKey] + + guard let urls: [URL] = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys, options: [.skipHiddenVolumes]) else { + return [] + } + + for url in urls { + do { + let resourceValues: URLResourceValues = try url.resourceValues(forKeys: Set(keys)) + + guard let volumeName: String = resourceValues.volumeName, + let volumeLocalizedFormatDescription: String = resourceValues.volumeLocalizedFormatDescription, + let volumeIsReadOnly: Bool = resourceValues.volumeIsReadOnly, + let volumeTotalCapacity: Int = resourceValues.volumeTotalCapacity, + volumeLocalizedFormatDescription == "Mac OS Extended (Journaled)", + !volumeIsReadOnly else { + continue + } + + let volume: InstallerVolume = InstallerVolume(id: UUID().uuidString, name: volumeName, path: url.path, capacity: UInt64(volumeTotalCapacity)) + volumes.append(volume) + } catch { + continue + } + } + + return volumes + } + + private func openDiskUtility() { + let url: URL = URL(fileURLWithPath: "/System/Applications/Utilities/Disk Utility.app") + NSWorkspace.shared.open(url) + } +} + +struct InstallerVolumeSelectionView_Previews: PreviewProvider { + static var previews: some View { + InstallerVolumeSelectionView(volume: .constant(.placeholder)) + } +} diff --git a/Mist/Views/List/ListRow.swift b/Mist/Views/List/ListRow.swift deleted file mode 100644 index 02c819f..0000000 --- a/Mist/Views/List/ListRow.swift +++ /dev/null @@ -1,273 +0,0 @@ -// -// ListRow.swift -// Mist -// -// Created by Nindi Gill on 28/6/2022. -// - -import Blessed -import SwiftUI -import System - -struct ListRow: View { - var type: DownloadType - var image: String - var version: String - var build: String - var beta: Bool - var date: String - var size: String - var compatible: Bool - @Binding var showPanel: Bool - @ObservedObject var taskManager: TaskManager - @State private var alertType: DownloadAlertType = .compatibility - @State private var showAlert: Bool = false - @AppStorage("cacheDownloads") - private var cacheDownloads: Bool = false - @AppStorage("cacheDirectory") - private var cacheDirectory: String = .cacheDirectory - private let length: CGFloat = 48 - private let spacing: CGFloat = 5 - private let padding: CGFloat = 3 - private var helpText: String { - """ - Version: \(version) - Build Number: \(build) - Release Date: \(date) - Download Size: \(size) - """ - } - private var compatibilityTitle: String { - "macOS \(type.description) not compatible!" - } - private var compatibilityMessage: String { - - guard let architecture: Architecture = Hardware.architecture else { - return "Invalid architecture!" - } - - let operation: String = type == .firmware ? "restore" : "re-install" - let string: String = "This macOS \(type.description) download cannot be used to \(operation) macOS on this \(architecture.description) Mac.\n\nAre you sure you want to continue?" - return string - } - private var privilegedHelperToolTitle: String { - "Privileged Helper Tool not installed!" - } - private var privilegedHelperToolMessage: String { - "The Mist Privileged Helper Tool is required to perform Administrator tasks when \(type == .firmware ? "downloading macOS Firmwares" : "creating macOS Installers")." - } - private var fullDiskAccessTitle: String { - "Full Disk Access required!" - } - private var fullDiskAccessMessage: String { - "Mist requires Full Disk Access to perform Administrator tasks when creating macOS Installers." - } - private var cacheDirectoryTitle: String { - "Cache directory settings incorrect!" - } - private var cacheDirectoryMessage: String { - "The cache directory has incorrect ownership and/or permissions, which will cause issues caching macOS Installers.\n\nRepair the cache directory ownership and/or permissions and try again." - } - - var body: some View { - HStack { - Group { - ZStack { - ScaledImage(name: image, length: length) - if beta { - TextRibbon(title: "BETA", length: length * 0.9) - } - } - HStack(spacing: spacing) { - Text(version) - .font(.title2) - Text("(\(build))") - .foregroundColor(.secondary) - } - Spacer() - Text(date) - .foregroundColor(.secondary) - Text(size) - } - .help(helpText) - .textSelection(.enabled) - Group { - switch type { - case .firmware: - Button { - compatible ? validate() : showCompatibilityWarning() - } label: { - Image(systemName: "arrow.down.circle") - .font(.body.bold()) - } - .help("Download macOS Firmware") - .buttonStyle(.capsule(.standard)) - case .installer: - HStack(spacing: 1) { - Button { - compatible ? validate() : showCompatibilityWarning() - } label: { - Image(systemName: "arrow.down.circle") - .font(.body.bold()) - } - .help("Download and export macOS Installer") - .buttonStyle(.capsule(.leading)) - Button { - print("Create bootable installer...") - } label: { - Image(systemName: "externaldrive") - .font(.body.bold()) - .padding(.vertical, 1) - } - .help("Create bootable macOS Installer") - .buttonStyle(.capsule(.trailing)) - } - } - } - .padding(.trailing, padding) - } - .alert(isPresented: $showAlert) { - switch alertType { - case .compatibility: - return Alert( - title: Text(compatibilityTitle), - message: Text(compatibilityMessage), - primaryButton: .default(Text("Cancel")), - secondaryButton: .default(Text("Continue")) { Task { validate() } } - ) - case .helperTool: - return Alert( - title: Text(privilegedHelperToolTitle), - message: Text(privilegedHelperToolMessage), - primaryButton: .default(Text("Install...")) { installPrivilegedHelperTool() }, - secondaryButton: .default(Text("Cancel")) - ) - case .fullDiskAccess: - return Alert( - title: Text(fullDiskAccessTitle), - message: Text(fullDiskAccessMessage), - primaryButton: .default(Text("Allow...")) { openFullDiskAccessPreferences() }, - secondaryButton: .default(Text("Cancel")) - ) - case .cacheDirectory: - return Alert( - title: Text(cacheDirectoryTitle), - message: Text(cacheDirectoryMessage), - primaryButton: .default(Text("Repair...")) { Task { try await repairCacheDirectoryOwnershipAndPermissions() } }, - secondaryButton: .default(Text("Cancel")) - ) - } - } - } - - private func showCompatibilityWarning() { - alertType = .compatibility - showAlert = true - } - - private func validate() { - - guard PrivilegedHelperTool.isInstalled() else { - alertType = .helperTool - showAlert = true - return - } - - if type == .installer { - - guard FileManager.default.isReadableFile(atPath: .tccDatabasePath) else { - alertType = .fullDiskAccess - showAlert = true - return - } - } - - if cacheDownloads { - - do { - var isDirectory: ObjCBool = false - - if !FileManager.default.fileExists(atPath: cacheDirectory, isDirectory: &isDirectory) { - try FileManager.default.createDirectory(atPath: cacheDirectory, withIntermediateDirectories: true) - } - - let attributes: [FileAttributeKey: Any] = try FileManager.default.attributesOfItem(atPath: cacheDirectory) - - guard let posixPermissions: NSNumber = attributes[.posixPermissions] as? NSNumber else { - alertType = .cacheDirectory - showAlert = true - return - } - - let filePermissions: FilePermissions = FilePermissions(rawValue: CModeT(posixPermissions.int16Value)) - - guard filePermissions == [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute], - let ownerAccountName: String = attributes[.ownerAccountName] as? String, - ownerAccountName == NSUserName(), - let groupOwnerAccountName: String = attributes[.groupOwnerAccountName] as? String, - groupOwnerAccountName == "wheel" else { - alertType = .cacheDirectory - showAlert = true - return - } - } catch { - alertType = .cacheDirectory - showAlert = true - return - } - } - - showPanel = true - } - - private func installPrivilegedHelperTool() { - try? PrivilegedHelperManager.shared.authorizeAndBless() - } - - private func openFullDiskAccessPreferences() { - - guard let url: URL = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") else { - return - } - - NSWorkspace.shared.open(url) - } - - private func repairCacheDirectoryOwnershipAndPermissions() async throws { - let url: URL = URL(fileURLWithPath: cacheDirectory) - let ownerAccountName: String = NSUserName() - try await FileAttributesUpdater.update(url: url, ownerAccountName: ownerAccountName) - } -} - -struct ListRow_Previews: PreviewProvider { - static let firmware: Firmware = .example - static let installer: Installer = .example - - static var previews: some View { - ListRow( - type: .firmware, - image: firmware.imageName, - version: firmware.version, - build: firmware.build, - beta: firmware.beta, - date: firmware.formattedDate, - size: firmware.size.bytesString(), - compatible: firmware.compatible, - showPanel: .constant(false), - taskManager: .shared - ) - ListRow( - type: .installer, - image: installer.imageName, - version: installer.version, - build: installer.build, - beta: installer.beta, - date: installer.date, - size: installer.size.bytesString(), - compatible: firmware.compatible, - showPanel: .constant(false), - taskManager: .shared - ) - } -} diff --git a/Mist/Views/List/ListRowDetail.swift b/Mist/Views/List/ListRowDetail.swift new file mode 100644 index 0000000..5143e31 --- /dev/null +++ b/Mist/Views/List/ListRowDetail.swift @@ -0,0 +1,69 @@ +// +// ListRowDetail.swift +// Mist +// +// Created by Nindi Gill on 12/6/2023. +// + +import SwiftUI + +struct ListRowDetail: View { + var imageName: String + var beta: Bool + var version: String + var build: String + var date: String + var size: String + var tooltip: String + private let length: CGFloat = 48 + private let spacing: CGFloat = 5 + + var body: some View { + HStack { + ZStack { + ScaledImage(name: imageName, length: length) + if beta { + TextRibbon(title: "BETA", length: length * 0.9) + } + } + HStack(spacing: spacing) { + Text(version) + .font(.title2) + Text("(\(build))") + .foregroundColor(.secondary) + } + Spacer() + Text(date) + .foregroundColor(.secondary) + Text(size) + } + .help(tooltip) + .textSelection(.enabled) + } +} + +struct ListRowDetail_Previews: PreviewProvider { + static let firmware: Firmware = .example + static let installer: Installer = .example + + static var previews: some View { + ListRowDetail( + imageName: firmware.imageName, + beta: firmware.beta, + version: firmware.version, + build: firmware.build, + date: firmware.formattedDate, + size: firmware.size.bytesString(), + tooltip: firmware.tooltip + ) + ListRowDetail( + imageName: installer.imageName, + beta: installer.beta, + version: installer.version, + build: installer.build, + date: installer.date, + size: installer.size.bytesString(), + tooltip: installer.tooltip + ) + } +} diff --git a/Mist/Views/List/ListRowFirmware.swift b/Mist/Views/List/ListRowFirmware.swift new file mode 100644 index 0000000..c61651b --- /dev/null +++ b/Mist/Views/List/ListRowFirmware.swift @@ -0,0 +1,144 @@ +// +// ListRowFirmware.swift +// Mist +// +// Created by Nindi Gill on 13/6/2022. +// + +import Blessed +import SwiftUI + +struct ListRowFirmware: View { + @AppStorage("firmwareFilename") + private var firmwareFilename: String = .firmwareFilenameTemplate + @AppStorage("retries") + private var retries: Int = 10 + @AppStorage("retryDelay") + private var retryDelay: Int = 30 + var firmware: Firmware + @Binding var savePanel: NSSavePanel + @Binding var tasksInProgress: Bool + @ObservedObject var taskManager: TaskManager + @State private var alertType: FirmwareAlertType = .compatibility + @State private var showAlert: Bool = false + @State private var showSavePanel: Bool = false + @State private var downloading: Bool = false + private let length: CGFloat = 48 + private let spacing: CGFloat = 5 + private let padding: CGFloat = 3 + private var compatibilityMessage: String { + + guard let architecture: Architecture = Hardware.architecture else { + return "Invalid architecture!" + } + + return "This macOS Firmware download cannot be used to restore macOS on this \(architecture.description) Mac.\n\nAre you sure you want to continue?" + } + + var body: some View { + HStack { + ListRowDetail( + imageName: firmware.imageName, + beta: firmware.beta, + version: firmware.version, + build: firmware.build, + date: firmware.formattedDate, + size: firmware.size.bytesString(), + tooltip: firmware.tooltip + ) + Button { + firmware.compatible ? validate() : showCompatibilityWarning() + } label: { + Image(systemName: "arrow.down.circle") + .font(.body.bold()) + } + .help("Download macOS Firmware") + .buttonStyle(.mistAction) + .clipShape(Capsule()) + } + .alert(isPresented: $showAlert) { + switch alertType { + case .compatibility: + return Alert( + title: Text("macOS Firmware not compatible!"), + message: Text(compatibilityMessage), + primaryButton: .default(Text("Cancel")), + secondaryButton: .default(Text("Continue")) { Task { validate() } } + ) + case .helperTool: + return Alert( + title: Text("Privileged Helper Tool not installed!"), + message: Text("The Mist Privileged Helper Tool is required to perform Administrator tasks when downloading macOS Firmwares"), + primaryButton: .default(Text("Install...")) { installPrivilegedHelperTool() }, + secondaryButton: .default(Text("Cancel")) + ) + } + } + .onChange(of: showSavePanel) { boolean in + + if boolean { + save() + } + } + .sheet(isPresented: $downloading) { + ActivityView( + downloadType: .firmware, + imageName: firmware.imageName, + name: firmware.name, + version: firmware.version, + build: firmware.build, + beta: firmware.beta, + destinationURL: savePanel.url, + taskManager: taskManager + ) + } + } + + private func save() { + showSavePanel = false + savePanel.title = "Download Firmware" + savePanel.nameFieldStringValue = firmwareFilename.stringWithSubstitutions(name: firmware.name, version: firmware.version, build: firmware.build) + savePanel.canCreateDirectories = true + savePanel.canSelectHiddenExtension = true + savePanel.isExtensionHidden = false + + Task { + let response: NSApplication.ModalResponse = savePanel.runModal() + + guard response == .OK else { + return + } + + taskManager.taskGroups = try TaskManager.taskGroups(for: firmware, destination: savePanel.url, retries: retries, delay: retryDelay) + downloading = true + tasksInProgress = true + } + } + + private func showCompatibilityWarning() { + alertType = .compatibility + showAlert = true + } + + private func validate() { + + guard PrivilegedHelperTool.isInstalled() else { + alertType = .helperTool + showAlert = true + return + } + + showSavePanel = true + } + + private func installPrivilegedHelperTool() { + try? PrivilegedHelperManager.shared.authorizeAndBless() + } +} + +struct ListRowFirmware_Previews: PreviewProvider { + + static var previews: some View { + ListRowFirmware(firmware: .example, savePanel: .constant(NSSavePanel()), tasksInProgress: .constant(false), taskManager: .shared) + } +} diff --git a/Mist/Views/List/ListRowInstaller.swift b/Mist/Views/List/ListRowInstaller.swift new file mode 100644 index 0000000..10dd5d1 --- /dev/null +++ b/Mist/Views/List/ListRowInstaller.swift @@ -0,0 +1,335 @@ +// +// ListRowInstaller.swift +// Mist +// +// Created by Nindi Gill on 17/6/2022. +// + +import Blessed +import SwiftUI +import System + +// swiftlint:disable:next type_body_length +struct ListRowInstaller: View { + @AppStorage("cacheDownloads") + private var cacheDownloads: Bool = false + @AppStorage("cacheDirectory") + private var cacheDirectory: String = .cacheDirectory + @AppStorage("applicationFilename") + private var applicationFilename: String = .applicationFilenameTemplate + @AppStorage("diskImageFilename") + private var diskImageFilename: String = .diskImageFilenameTemplate + @AppStorage("diskImageSign") + private var diskImageSign: Bool = false + @AppStorage("diskImageSigningIdentity") + private var diskImageSigningIdentity: String = "" + @AppStorage("isoFilename") + private var isoFilename: String = .isoFilenameTemplate + @AppStorage("packageFilename") + private var packageFilename: String = .packageFilenameTemplate + @AppStorage("packageIdentifier") + private var packageIdentifier: String = .packageIdentifierTemplate + @AppStorage("packageSign") + private var packageSign: Bool = false + @AppStorage("packageSigningIdentity") + private var packageSigningIdentity: String = "" + @AppStorage("retries") + private var retries: Int = 10 + @AppStorage("retryDelay") + private var retryDelay: Int = 30 + var installer: Installer + @Binding var openPanel: NSOpenPanel + @Binding var tasksInProgress: Bool + @ObservedObject var taskManager: TaskManager + @State private var alertType: InstallerAlertType = .compatibility + @State private var showAlert: Bool = false + @State private var sheetType: InstallerSheetType = .download + @State private var showSheet: Bool = false + @State private var showOpenPanel: Bool = false + @State private var exports: [InstallerExportType] = [] + @State private var volume: InstallerVolume? + private let length: CGFloat = 48 + private let spacing: CGFloat = 5 + private let padding: CGFloat = 3 + private var compatibilityMessage: String { + + guard let architecture: Architecture = Hardware.architecture else { + return "Invalid architecture!" + } + + return "This macOS Installer download cannot be used to restore macOS on this \(architecture.description) Mac.\n\nAre you sure you want to continue?" + } + private var cacheDirectoryMessage: String { + "The cache directory has incorrect ownership and/or permissions, which will cause issues caching macOS Installers.\n\nRepair the cache directory ownership and/or permissions and try again." + } + + var body: some View { + HStack { + ListRowDetail( + imageName: installer.imageName, + beta: installer.beta, + version: installer.version, + build: installer.build, + date: installer.date, + size: installer.size.bytesString(), + tooltip: installer.tooltip + ) + HStack(spacing: 1) { + Button { + pressButton(.download) + } label: { + Image(systemName: "arrow.down.circle").font(.body.bold()) + } + .help("Download and export macOS Installer") + .buttonStyle(.mistAction) + if let architecture: Architecture = Hardware.architecture, + (architecture == .appleSilicon && installer.bigSurOrNewer) || (architecture == .intel && installer.mavericksOrNewer) { + Button { + pressButton(.volumeSelection) + } label: { + Image(systemName: "externaldrive").font(.body.bold()) + .padding(.vertical, 1) + } + .help("Create bootable macOS Installer") + .buttonStyle(.mistAction) + } + } + .clipShape(Capsule()) + } + .alert(isPresented: $showAlert) { + switch alertType { + case .compatibility: + return Alert( + title: Text("macOS Installer not compatible!"), + message: Text(compatibilityMessage), + primaryButton: .default(Text("Cancel")), + secondaryButton: .default(Text("Continue")) { Task { validate() } } + ) + case .helperTool: + return Alert( + title: Text("Privileged Helper Tool not installed!"), + message: Text("The Mist Privileged Helper Tool is required to perform Administrator tasks when creating macOS Installers."), + primaryButton: .default(Text("Install...")) { installPrivilegedHelperTool() }, + secondaryButton: .default(Text("Cancel")) + ) + case .fullDiskAccess: + return Alert( + title: Text("Full Disk Access required!"), + message: Text("Mist requires Full Disk Access to perform Administrator tasks when creating macOS Installers."), + primaryButton: .default(Text("Allow...")) { openFullDiskAccessPreferences() }, + secondaryButton: .default(Text("Cancel")) + ) + case .cacheDirectory: + return Alert( + title: Text("Cache directory settings incorrect!"), + message: Text(cacheDirectoryMessage), + primaryButton: .default(Text("Repair...")) { Task { try await repairCacheDirectoryOwnershipAndPermissions() } }, + secondaryButton: .default(Text("Cancel")) + ) + } + } + .onChange(of: showOpenPanel) { boolean in + + if boolean { + open() + } + } + .onChange(of: volume) { volume in + + if volume != nil { + createBootableInstaller() + } + } + .onChange(of: sheetType) { _ in } // hack to make cascading sheets work + .sheet(isPresented: $showSheet) { + switch sheetType { + case .download: + ActivityView( + downloadType: .installer, + imageName: installer.imageName, + name: installer.name.replacingOccurrences(of: " beta", with: ""), + version: installer.version, + build: installer.build, + beta: installer.beta, + destinationURL: openPanel.url, + taskManager: taskManager + ) + case .volumeSelection: + InstallerVolumeSelectionView(volume: $volume) + case .createBootableInstaller: + ActivityView( + downloadType: .installer, + imageName: installer.imageName, + name: installer.name.replacingOccurrences(of: " beta", with: ""), + version: installer.version, + build: installer.build, + beta: installer.beta, + destinationURL: URL(fileURLWithPath: "/Volumes/Install \(installer.name)"), + taskManager: taskManager + ) + } + } + } + + private func pressButton(_ type: InstallerSheetType) { + sheetType = type + + if installer.compatible { + Task { validate() } + } else { + showCompatibilityWarning() + } + } + + private func open() { + showOpenPanel = false + openPanel.title = "Download Installer" + openPanel.canChooseFiles = false + openPanel.canChooseDirectories = true + openPanel.allowsMultipleSelection = false + openPanel.prompt = "Save" + openPanel.accessoryView = NSHostingView(rootView: InstallerExportView(installer: installer, exports: $exports)) + openPanel.isAccessoryViewDisclosed = true + + Task { + let response: NSApplication.ModalResponse = openPanel.runModal() + + guard response == .OK else { + return + } + + taskManager.taskGroups = try TaskManager.taskGroups( + for: installer, + destination: openPanel.url, + exports: exports, + cacheDownloads: cacheDownloads, + cacheDirectory: cacheDirectory, + retries: retries, + delay: retryDelay, + applicationFilename: applicationFilename, + diskImageFilename: diskImageFilename, + diskImageSign: diskImageSign, + diskImageSigningIdentity: diskImageSigningIdentity, + isoFilename: isoFilename, + packageFilename: packageFilename, + packageIdentifier: packageIdentifier, + packageSign: packageSign, + packageSigningIdentity: packageSigningIdentity + ) + + showSheet = true + tasksInProgress = true + } + } + + private func createBootableInstaller() { + + guard let volume: InstallerVolume = volume else { + return + } + + Task { + taskManager.taskGroups = try TaskManager.taskGroups( + for: installer, + cacheDownloads: cacheDownloads, + cacheDirectory: cacheDirectory, + retries: retries, + delay: retryDelay, + volume: volume + ) + + sheetType = .createBootableInstaller + showSheet = true + tasksInProgress = true + } + } + + private func showCompatibilityWarning() { + alertType = .compatibility + showAlert = true + } + + private func validate() { + + guard PrivilegedHelperTool.isInstalled() else { + alertType = .helperTool + showAlert = true + return + } + + guard FileManager.default.isReadableFile(atPath: .tccDatabasePath) else { + alertType = .fullDiskAccess + showAlert = true + return + } + + if cacheDownloads { + + do { + var isDirectory: ObjCBool = false + + if !FileManager.default.fileExists(atPath: cacheDirectory, isDirectory: &isDirectory) { + try FileManager.default.createDirectory(atPath: cacheDirectory, withIntermediateDirectories: true) + } + + let attributes: [FileAttributeKey: Any] = try FileManager.default.attributesOfItem(atPath: cacheDirectory) + + guard let posixPermissions: NSNumber = attributes[.posixPermissions] as? NSNumber else { + alertType = .cacheDirectory + showAlert = true + return + } + + let filePermissions: FilePermissions = FilePermissions(rawValue: CModeT(posixPermissions.int16Value)) + + guard filePermissions == [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute], + let ownerAccountName: String = attributes[.ownerAccountName] as? String, + ownerAccountName == NSUserName(), + let groupOwnerAccountName: String = attributes[.groupOwnerAccountName] as? String, + groupOwnerAccountName == "wheel" else { + alertType = .cacheDirectory + showAlert = true + return + } + } catch { + alertType = .cacheDirectory + showAlert = true + return + } + } + + switch sheetType { + case .download: + showOpenPanel = true + case .volumeSelection: + showSheet = true + case .createBootableInstaller: + break + } + } + + private func installPrivilegedHelperTool() { + try? PrivilegedHelperManager.shared.authorizeAndBless() + } + + private func openFullDiskAccessPreferences() { + + guard let url: URL = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") else { + return + } + + NSWorkspace.shared.open(url) + } + + private func repairCacheDirectoryOwnershipAndPermissions() async throws { + let url: URL = URL(fileURLWithPath: cacheDirectory) + let ownerAccountName: String = NSUserName() + try await FileAttributesUpdater.update(url: url, ownerAccountName: ownerAccountName) + } +} + +struct ListRowInstaller_Previews: PreviewProvider { + static var previews: some View { + ListRowInstaller(installer: .example, openPanel: .constant(NSOpenPanel()), tasksInProgress: .constant(false), taskManager: .shared) + } +}