From d2fb20749981e48c9206ec7b5ae48ea18970d4c4 Mon Sep 17 00:00:00 2001 From: Nindi Gill Date: Tue, 13 Jun 2023 16:57:15 +1000 Subject: [PATCH 01/10] Refactor to support bootable installers --- Mist.xcodeproj/project.pbxproj | 102 +++--- Mist/AppCommands.swift | 6 +- .../Bootable Installer.png | Bin 0 -> 74033 bytes .../Bootable Installer.imageset/Contents.json | 12 + Mist/Helpers/TaskManager.swift | 50 +++ Mist/MistApp.swift | 6 +- Mist/Model/Firmware.swift | 8 + Mist/Model/FirmwareAlertType.swift | 13 + Mist/Model/Installer.swift | 8 + ...ertType.swift => InstallerAlertType.swift} | 4 +- Mist/Model/InstallerSheetType.swift | 14 + Mist/Model/InstallerVolume.swift | 18 + Mist/Model/MistTaskSection.swift | 1 + .../ActivityHeaderView.swift} | 10 +- .../ActivityProgressView.swift} | 10 +- .../ActivityRowView.swift} | 8 +- .../ActivitySectionHeaderView.swift} | 8 +- .../ActivityView.swift} | 36 +- Mist/Views/ContentView.swift | 9 +- Mist/Views/List/FirmwareListRow.swift | 84 ----- Mist/Views/List/InstallerListRow.swift | 124 ------- ...tallerVolumeSelectionInformationView.swift | 37 ++ .../InstallerVolumeSelectionPickerView.swift | 49 +++ .../List/InstallerVolumeSelectionView.swift | 108 ++++++ Mist/Views/List/ListRow.swift | 273 --------------- Mist/Views/List/ListRowDetail.swift | 69 ++++ Mist/Views/List/ListRowFirmware.swift | 144 ++++++++ Mist/Views/List/ListRowInstaller.swift | 325 ++++++++++++++++++ 28 files changed, 971 insertions(+), 565 deletions(-) create mode 100644 Mist/Assets.xcassets/Bootable Installer.imageset/Bootable Installer.png create mode 100644 Mist/Assets.xcassets/Bootable Installer.imageset/Contents.json create mode 100644 Mist/Model/FirmwareAlertType.swift rename Mist/Model/{DownloadAlertType.swift => InstallerAlertType.swift} (78%) create mode 100644 Mist/Model/InstallerSheetType.swift create mode 100644 Mist/Model/InstallerVolume.swift rename Mist/Views/{Download/DownloadHeaderView.swift => Activity/ActivityHeaderView.swift} (79%) rename Mist/Views/{Download/DownloadProgressView.swift => Activity/ActivityProgressView.swift} (81%) rename Mist/Views/{Download/DownloadRowView.swift => Activity/ActivityRowView.swift} (84%) rename Mist/Views/{Download/DownloadSectionHeaderView.swift => Activity/ActivitySectionHeaderView.swift} (73%) rename Mist/Views/{Download/DownloadView.swift => Activity/ActivityView.swift} (89%) delete mode 100644 Mist/Views/List/FirmwareListRow.swift delete mode 100644 Mist/Views/List/InstallerListRow.swift create mode 100644 Mist/Views/List/InstallerVolumeSelectionInformationView.swift create mode 100644 Mist/Views/List/InstallerVolumeSelectionPickerView.swift create mode 100644 Mist/Views/List/InstallerVolumeSelectionView.swift delete mode 100644 Mist/Views/List/ListRow.swift create mode 100644 Mist/Views/List/ListRowDetail.swift create mode 100644 Mist/Views/List/ListRowFirmware.swift create mode 100644 Mist/Views/List/ListRowInstaller.swift diff --git a/Mist.xcodeproj/project.pbxproj b/Mist.xcodeproj/project.pbxproj index 227c3f9..73b3f7c 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 */; }; @@ -133,6 +132,13 @@ 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 */; }; @@ -170,7 +176,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 +214,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 +223,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 = ""; }; @@ -280,6 +285,13 @@ 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 = ""; }; @@ -412,13 +424,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 +455,7 @@ 3935F49C286ABE4D00760AB0 /* FooterView.swift */, 390451D728573A2500E0B563 /* ExportListView.swift */, 3935F4A0286ACCE100760AB0 /* List */, - 393F35BF28641D86005B7165 /* Download */, + 393F35BF28641D86005B7165 /* Activity */, 393F35C028641D8F005B7165 /* Refresh */, 39FF05F2285984F800A86670 /* Settings */, 39FF05F12859849200A86670 /* Components */, @@ -451,25 +466,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 */ = { @@ -765,6 +783,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,7 +793,8 @@ 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 */, @@ -788,29 +808,29 @@ 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 +853,30 @@ 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 +885,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 +895,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 0000000000000000000000000000000000000000..e82d859fa01ccc5d6dd3bbdf340deb959d592223 GIT binary patch literal 74033 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ⅈ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
-U3sb2=Z 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 literal 0 HcmV?d00001 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/Helpers/TaskManager.swift b/Mist/Helpers/TaskManager.swift index 9ea6c27..cae90d3 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] = [] @@ -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) + } + ] + + 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..2e5d0b0 100644 --- a/Mist/Model/Installer.swift +++ b/Mist/Model/Installer.swift @@ -775,6 +775,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 89% rename from Mist/Views/Download/DownloadView.swift rename to Mist/Views/Activity/ActivityView.swift index fb9f0af..a37fe13 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,9 @@ 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 buttonText: String { switch taskManager.currentState { case .pending, .inProgress: @@ -47,21 +50,18 @@ 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) - } - if index < taskGroup.tasks.count - 1 { - Divider() + ActivityProgressView(state: taskGroup.tasks[index].state, value: value, size: size) } } .id("\(taskGroup.section.id).\(index)") @@ -70,7 +70,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 +157,7 @@ struct DownloadView: View { } if showInFinder { + guard let url: URL = destinationURL else { return } @@ -192,7 +193,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 +226,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/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/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..81429b1 --- /dev/null +++ b/Mist/Views/List/InstallerVolumeSelectionView.swift @@ -0,0 +1,108 @@ +// +// 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, .volumeIsRemovableKey, .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 volumeIsRemovable: Bool = resourceValues.volumeIsRemovable, + let volumeTotalCapacity: Int = resourceValues.volumeTotalCapacity, + volumeLocalizedFormatDescription == "Mac OS Extended (Journaled)", + !volumeIsReadOnly, + volumeIsRemovable 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..c19f6a2 --- /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(.capsule(.standard)) + .padding(.trailing, padding) + } + .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..f75c305 --- /dev/null +++ b/Mist/Views/List/ListRowInstaller.swift @@ -0,0 +1,325 @@ +// +// 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 { + sheetType = .download + if installer.compatible { Task { validate() } } else { showCompatibilityWarning() } + } label: { + Image(systemName: "arrow.down.circle") + .font(.body.bold()) + } + .help("Download and export macOS Installer") + .buttonStyle(.capsule(.leading)) + Button { + sheetType = .volumeSelection + if installer.compatible { Task { validate() } } else { showCompatibilityWarning() } + } label: { + Image(systemName: "externaldrive") + .font(.body.bold()) + .padding(.vertical, 1) + } + .help("Create bootable macOS Installer") + .buttonStyle(.capsule(.trailing)) + } + } + .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/"), + 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 + ) + + 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) + } +} From 26b84e29c7b935ba9569c8fa0bb6e2edb76467cb Mon Sep 17 00:00:00 2001 From: Nindi Gill Date: Tue, 13 Jun 2023 17:41:25 +1000 Subject: [PATCH 02/10] Fix support for legacy bootable installers --- Mist/Helpers/InstallMediaCreator.swift | 15 +++++++++++---- Mist/Helpers/TaskManager.swift | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) 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 cae90d3..36232c4 100644 --- a/Mist/Helpers/TaskManager.swift +++ b/Mist/Helpers/TaskManager.swift @@ -441,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) { @@ -513,7 +513,7 @@ class TaskManager: ObservableObject { let mountPointURL: URL = URL(fileURLWithPath: volume.path) let tasks: [MistTask] = [ MistTask(type: .create, description: "Bootable Installer") { - try await InstallMediaCreator.create(createInstallMediaURL, mountPoint: mountPointURL) + try await InstallMediaCreator.create(createInstallMediaURL, mountPoint: mountPointURL, sierraOrOlder: installer.sierraOrOlder) } ] From 94f45e0d002bf6311ec8aef07f7f6a418bda723a Mon Sep 17 00:00:00 2001 From: Nindi Gill Date: Tue, 13 Jun 2023 18:26:40 +1000 Subject: [PATCH 03/10] Fix capsule button styles on macOS Monterey + Ventura --- Mist.xcodeproj/project.pbxproj | 28 +++--------------- Mist/Extensions/ButtonStyle+Extension.swift | 6 ++-- .../Capsule/CapsuleButtonStyleType.swift | 18 ------------ .../Components/Capsule/CapsuleLeading.swift | 29 ------------------- .../Components/Capsule/CapsuleTrailing.swift | 29 ------------------- ...tyle.swift => MistActionButtonStyle.swift} | 19 ++---------- Mist/Views/List/ListRowFirmware.swift | 3 +- Mist/Views/List/ListRowInstaller.swift | 5 ++-- 8 files changed, 15 insertions(+), 122 deletions(-) delete mode 100644 Mist/Views/Components/Capsule/CapsuleButtonStyleType.swift delete mode 100644 Mist/Views/Components/Capsule/CapsuleLeading.swift delete mode 100644 Mist/Views/Components/Capsule/CapsuleTrailing.swift rename Mist/Views/Components/{Capsule/CapsuleButtonStyle.swift => MistActionButtonStyle.swift} (50%) diff --git a/Mist.xcodeproj/project.pbxproj b/Mist.xcodeproj/project.pbxproj index 73b3f7c..02e3533 100644 --- a/Mist.xcodeproj/project.pbxproj +++ b/Mist.xcodeproj/project.pbxproj @@ -131,7 +131,6 @@ 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 */; }; @@ -140,9 +139,7 @@ 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 */ @@ -284,7 +281,6 @@ 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 = ""; }; @@ -293,9 +289,7 @@ 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 */ @@ -534,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 */, @@ -572,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 */ @@ -799,7 +782,7 @@ 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 */, @@ -826,7 +809,6 @@ 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 /* ActivityHeaderView.swift in Sources */, 3935F480286551FB00760AB0 /* Double+Extension.swift in Sources */, @@ -863,7 +845,6 @@ 39148CFC28DD55B300011FF5 /* PathControl.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 */, @@ -871,7 +852,6 @@ 575812C02A37493F00425BAF /* InstallerVolumeSelectionView.swift in Sources */, 390451C22856E3F500E0B563 /* Hardware.swift in Sources */, 39CF56092861AE7F006FB5D2 /* HelperToolCommandRequest.swift in Sources */, - 57CF961A2A34B65C008D3B1C /* CapsuleLeading.swift in Sources */, 390451C82856E94900E0B563 /* ListRowFirmware.swift in Sources */, 390451E528574F0000E0B563 /* CatalogType.swift in Sources */, 3935F4852866B64900760AB0 /* MistTaskSection.swift in Sources */, 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/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/List/ListRowFirmware.swift b/Mist/Views/List/ListRowFirmware.swift index c19f6a2..8e251cb 100644 --- a/Mist/Views/List/ListRowFirmware.swift +++ b/Mist/Views/List/ListRowFirmware.swift @@ -53,7 +53,8 @@ struct ListRowFirmware: View { .font(.body.bold()) } .help("Download macOS Firmware") - .buttonStyle(.capsule(.standard)) + .buttonStyle(.mistAction) + .clipShape(Capsule()) .padding(.trailing, padding) } .alert(isPresented: $showAlert) { diff --git a/Mist/Views/List/ListRowInstaller.swift b/Mist/Views/List/ListRowInstaller.swift index f75c305..b955328 100644 --- a/Mist/Views/List/ListRowInstaller.swift +++ b/Mist/Views/List/ListRowInstaller.swift @@ -83,7 +83,7 @@ struct ListRowInstaller: View { .font(.body.bold()) } .help("Download and export macOS Installer") - .buttonStyle(.capsule(.leading)) + .buttonStyle(.mistAction) Button { sheetType = .volumeSelection if installer.compatible { Task { validate() } } else { showCompatibilityWarning() } @@ -93,8 +93,9 @@ struct ListRowInstaller: View { .padding(.vertical, 1) } .help("Create bootable macOS Installer") - .buttonStyle(.capsule(.trailing)) + .buttonStyle(.mistAction) } + .clipShape(Capsule()) } .alert(isPresented: $showAlert) { switch alertType { From 273bd7692c095aeccc2a0fcda1ed5f69959ca673 Mon Sep 17 00:00:00 2001 From: Nindi Gill Date: Tue, 13 Jun 2023 18:26:54 +1000 Subject: [PATCH 04/10] Fix activity view dividers on macOS Ventura + Monterey --- Mist/Views/Activity/ActivityView.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Mist/Views/Activity/ActivityView.swift b/Mist/Views/Activity/ActivityView.swift index a37fe13..ef14a04 100644 --- a/Mist/Views/Activity/ActivityView.swift +++ b/Mist/Views/Activity/ActivityView.swift @@ -38,6 +38,9 @@ struct ActivityView: View { 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: @@ -63,6 +66,9 @@ struct ActivityView: View { let size: UInt64 = taskGroup.tasks[index].downloadSize { ActivityProgressView(state: taskGroup.tasks[index].state, value: value, size: size) } + if venturaOrOlder && index != taskGroup.tasks.count { + Divider() + } } .id("\(taskGroup.section.id).\(index)") } From f42acf2e0019e99a6799266060187875589e376a Mon Sep 17 00:00:00 2001 From: Nindi Gill Date: Tue, 13 Jun 2023 18:31:26 +1000 Subject: [PATCH 05/10] Remove firmware download button padding --- Mist/Views/List/ListRowFirmware.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Mist/Views/List/ListRowFirmware.swift b/Mist/Views/List/ListRowFirmware.swift index 8e251cb..c61651b 100644 --- a/Mist/Views/List/ListRowFirmware.swift +++ b/Mist/Views/List/ListRowFirmware.swift @@ -55,7 +55,6 @@ struct ListRowFirmware: View { .help("Download macOS Firmware") .buttonStyle(.mistAction) .clipShape(Capsule()) - .padding(.trailing, padding) } .alert(isPresented: $showAlert) { switch alertType { From bcdaccb3425977bde25bdd00aa4882b0e32c771d Mon Sep 17 00:00:00 2001 From: Nindi Gill Date: Tue, 13 Jun 2023 18:39:26 +1000 Subject: [PATCH 06/10] Fix linter warning --- Mist/Views/List/ListRowInstaller.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Mist/Views/List/ListRowInstaller.swift b/Mist/Views/List/ListRowInstaller.swift index b955328..b082881 100644 --- a/Mist/Views/List/ListRowInstaller.swift +++ b/Mist/Views/List/ListRowInstaller.swift @@ -76,8 +76,7 @@ struct ListRowInstaller: View { ) HStack(spacing: 1) { Button { - sheetType = .download - if installer.compatible { Task { validate() } } else { showCompatibilityWarning() } + pressButton(.download) } label: { Image(systemName: "arrow.down.circle") .font(.body.bold()) @@ -85,8 +84,7 @@ struct ListRowInstaller: View { .help("Download and export macOS Installer") .buttonStyle(.mistAction) Button { - sheetType = .volumeSelection - if installer.compatible { Task { validate() } } else { showCompatibilityWarning() } + pressButton(.volumeSelection) } label: { Image(systemName: "externaldrive") .font(.body.bold()) @@ -172,6 +170,16 @@ struct ListRowInstaller: View { } } + private func pressButton(_ type: InstallerSheetType) { + sheetType = type + + if installer.compatible { + Task { validate() } + } else { + showCompatibilityWarning() + } + } + private func open() { showOpenPanel = false openPanel.title = "Download Installer" From fbc6767df1c11bca000347d479e953a238a71c51 Mon Sep 17 00:00:00 2001 From: Nindi Gill Date: Tue, 13 Jun 2023 18:44:33 +1000 Subject: [PATCH 07/10] Fix bootable installer mount point URL --- Mist/Views/List/ListRowInstaller.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mist/Views/List/ListRowInstaller.swift b/Mist/Views/List/ListRowInstaller.swift index b082881..f3ed2e0 100644 --- a/Mist/Views/List/ListRowInstaller.swift +++ b/Mist/Views/List/ListRowInstaller.swift @@ -163,7 +163,7 @@ struct ListRowInstaller: View { version: installer.version, build: installer.build, beta: installer.beta, - destinationURL: URL(fileURLWithPath: "/Volumes/"), + destinationURL: URL(fileURLWithPath: "/Volumes/Install \(installer.name)"), taskManager: taskManager ) } From 6b4aec4e5016552c62d927a644c5e1af57e3c026 Mon Sep 17 00:00:00 2001 From: Nindi Gill Date: Tue, 13 Jun 2023 18:58:19 +1000 Subject: [PATCH 08/10] Hide bootable installer button for macOS Catalina and older --- Mist/Views/List/ListRowInstaller.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Mist/Views/List/ListRowInstaller.swift b/Mist/Views/List/ListRowInstaller.swift index f3ed2e0..1086072 100644 --- a/Mist/Views/List/ListRowInstaller.swift +++ b/Mist/Views/List/ListRowInstaller.swift @@ -83,15 +83,17 @@ struct ListRowInstaller: View { } .help("Download and export macOS Installer") .buttonStyle(.mistAction) - Button { - pressButton(.volumeSelection) - } label: { - Image(systemName: "externaldrive") - .font(.body.bold()) - .padding(.vertical, 1) + if installer.bigSurOrNewer { + Button { + pressButton(.volumeSelection) + } label: { + Image(systemName: "externaldrive") + .font(.body.bold()) + .padding(.vertical, 1) + } + .help("Create bootable macOS Installer") + .buttonStyle(.mistAction) } - .help("Create bootable macOS Installer") - .buttonStyle(.mistAction) } .clipShape(Capsule()) } From e5378b599bc4315fa9d5b68307872a60e666f6a4 Mon Sep 17 00:00:00 2001 From: Nindi Gill Date: Tue, 13 Jun 2023 20:26:20 +1000 Subject: [PATCH 09/10] Fix ISO compatibility messages --- Mist/Model/Installer.swift | 3 +++ Mist/Views/List/InstallerExportView.swift | 24 +++++++++++++++++------ Mist/Views/List/ListRowInstaller.swift | 9 ++++----- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/Mist/Model/Installer.swift b/Mist/Model/Installer.swift index 2e5d0b0..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 } 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/ListRowInstaller.swift b/Mist/Views/List/ListRowInstaller.swift index 1086072..10dd5d1 100644 --- a/Mist/Views/List/ListRowInstaller.swift +++ b/Mist/Views/List/ListRowInstaller.swift @@ -78,17 +78,16 @@ struct ListRowInstaller: View { Button { pressButton(.download) } label: { - Image(systemName: "arrow.down.circle") - .font(.body.bold()) + Image(systemName: "arrow.down.circle").font(.body.bold()) } .help("Download and export macOS Installer") .buttonStyle(.mistAction) - if installer.bigSurOrNewer { + if let architecture: Architecture = Hardware.architecture, + (architecture == .appleSilicon && installer.bigSurOrNewer) || (architecture == .intel && installer.mavericksOrNewer) { Button { pressButton(.volumeSelection) } label: { - Image(systemName: "externaldrive") - .font(.body.bold()) + Image(systemName: "externaldrive").font(.body.bold()) .padding(.vertical, 1) } .help("Create bootable macOS Installer") From 0cb683a8ad08625dd887993e72b797e984d22d55 Mon Sep 17 00:00:00 2001 From: Nindi Gill Date: Tue, 13 Jun 2023 22:01:20 +1000 Subject: [PATCH 10/10] Remove `volumeIsRemovableKey` --- Mist/Views/List/InstallerVolumeSelectionView.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Mist/Views/List/InstallerVolumeSelectionView.swift b/Mist/Views/List/InstallerVolumeSelectionView.swift index 81429b1..ca94f3f 100644 --- a/Mist/Views/List/InstallerVolumeSelectionView.swift +++ b/Mist/Views/List/InstallerVolumeSelectionView.swift @@ -64,7 +64,7 @@ struct InstallerVolumeSelectionView: View { private func getAvailableVolumes() -> [InstallerVolume] { var volumes: [InstallerVolume] = [] - let keys: [URLResourceKey] = [.volumeNameKey, .volumeLocalizedFormatDescriptionKey, .volumeIsReadOnlyKey, .volumeIsRemovableKey, .volumeTotalCapacityKey] + let keys: [URLResourceKey] = [.volumeNameKey, .volumeLocalizedFormatDescriptionKey, .volumeIsReadOnlyKey, .volumeTotalCapacityKey] guard let urls: [URL] = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys, options: [.skipHiddenVolumes]) else { return [] @@ -77,11 +77,9 @@ struct InstallerVolumeSelectionView: View { guard let volumeName: String = resourceValues.volumeName, let volumeLocalizedFormatDescription: String = resourceValues.volumeLocalizedFormatDescription, let volumeIsReadOnly: Bool = resourceValues.volumeIsReadOnly, - let volumeIsRemovable: Bool = resourceValues.volumeIsRemovable, let volumeTotalCapacity: Int = resourceValues.volumeTotalCapacity, volumeLocalizedFormatDescription == "Mac OS Extended (Journaled)", - !volumeIsReadOnly, - volumeIsRemovable else { + !volumeIsReadOnly else { continue }