VMDrivesSettingsView.swift 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. //
  2. // Copyright © 2020 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import SwiftUI
  17. // MARK: - Drives list
  18. struct VMDrivesSettingsView: View {
  19. @ObservedObject var config: UTMQemuConfiguration
  20. @Binding var isCreateDriveShown: Bool
  21. @Binding var isImportDriveShown: Bool
  22. @State private var attemptDelete: IndexSet?
  23. @EnvironmentObject private var data: UTMData
  24. var body: some View {
  25. ForEach($config.drives) { $drive in
  26. NavigationLink(
  27. destination: VMConfigDriveDetailsView(config: $drive, requestDriveDelete: .constant(nil)), label: {
  28. Label(title: { labelTitle(for: drive) }, icon: { Image(systemName: "externaldrive") })
  29. })
  30. }.onDelete { offsets in
  31. attemptDelete = offsets
  32. }
  33. .onMove(perform: moveDrives)
  34. Button {
  35. isImportDriveShown.toggle()
  36. } label: {
  37. Text("Import Drive…")
  38. }
  39. Button {
  40. isCreateDriveShown.toggle()
  41. } label: {
  42. Text("New Drive…")
  43. }
  44. .nonbrokenSheet(isPresented: $isCreateDriveShown) {
  45. CreateDrive(newDrive: UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target), onDismiss: newDrive)
  46. }
  47. .globalFileImporter(isPresented: $isImportDriveShown, allowedContentTypes: [.item], onCompletion: importDrive)
  48. .actionSheet(item: $attemptDelete) { offsets in
  49. ActionSheet(title: Text("Confirm Delete"), message: Text("Are you sure you want to permanently delete this disk image?"), buttons: [.cancel(), .destructive(Text("Delete")) {
  50. deleteDrives(offsets: offsets)
  51. }])
  52. }
  53. }
  54. private func labelTitle(for drive: UTMQemuConfigurationDrive) -> Text {
  55. if drive.interface == .none && drive.imageName == QEMUPackageFileName.efiVariables.rawValue {
  56. return Text("EFI Variables", comment: "VMDrivesSettingsView")
  57. } else {
  58. return Text("\(drive.interface.prettyValue) Drive", comment: "VMDrivesSettingsView")
  59. }
  60. }
  61. private func newDrive(drive: UTMQemuConfigurationDrive) {
  62. config.drives.append(drive)
  63. }
  64. private func deleteDrives(offsets: IndexSet) {
  65. config.drives.remove(atOffsets: offsets)
  66. }
  67. private func moveDrives(source: IndexSet, destination: Int) {
  68. config.drives.move(fromOffsets: source, toOffset: destination)
  69. }
  70. private func importDrive(result: Result<URL, Error>) {
  71. data.busyWorkAsync {
  72. switch result {
  73. case .success(let url):
  74. await MainActor.run {
  75. var drive = UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target, isExternal: false)
  76. drive.imageURL = url
  77. config.drives.append(drive)
  78. }
  79. break
  80. case .failure(let err):
  81. throw err
  82. }
  83. }
  84. }
  85. }
  86. // MARK: - Create Drive
  87. private extension View {
  88. /// A sheet that isn't broken on older versions.
  89. ///
  90. /// On iOS 14 and older, .sheet() breaks the table layout for some reason.
  91. /// This workarounds it by putting the sheet inside an overlay which does
  92. /// not affect displaying the sheet at all.
  93. /// - Parameters:
  94. /// - isPresented: same as .sheet()
  95. /// - onDismiss: same as .sheet()
  96. /// - content: same as .sheet()
  97. /// - Returns: same as .sheet()
  98. @ViewBuilder func nonbrokenSheet<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content : View {
  99. if #available(iOS 15, macOS 12, *) {
  100. self.sheet(isPresented: isPresented, onDismiss: onDismiss, content: content)
  101. } else {
  102. self.overlay(EmptyView().sheet(isPresented: isPresented, onDismiss: onDismiss, content: content))
  103. }
  104. }
  105. }
  106. private struct CreateDrive: View {
  107. @State var newDrive: UTMQemuConfigurationDrive
  108. let onDismiss: (UTMQemuConfigurationDrive) -> Void
  109. @Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
  110. var body: some View {
  111. NavigationView {
  112. VMConfigDriveCreateView(config: $newDrive)
  113. .toolbar {
  114. ToolbarItem(placement: .cancellationAction) {
  115. Button("Cancel", action: cancel)
  116. }
  117. ToolbarItem(placement: .confirmationAction) {
  118. Button("Done", action: done)
  119. }
  120. }
  121. }.navigationViewStyle(.stack)
  122. }
  123. private func cancel() {
  124. presentationMode.wrappedValue.dismiss()
  125. }
  126. private func done() {
  127. presentationMode.wrappedValue.dismiss()
  128. onDismiss(newDrive)
  129. }
  130. }
  131. // MARK: - Preview
  132. struct VMConfigDrivesView_Previews: PreviewProvider {
  133. @StateObject static private var config = UTMQemuConfiguration()
  134. static var previews: some View {
  135. Group {
  136. VMDrivesSettingsView(config: config, isCreateDriveShown: .constant(false), isImportDriveShown: .constant(false))
  137. CreateDrive(newDrive: UTMQemuConfigurationDrive()) { _ in
  138. }
  139. }.onAppear {
  140. if config.drives.count == 0 {
  141. var drive = UTMQemuConfigurationDrive(forArchitecture: .x86_64, target: QEMUTarget_x86_64.pc)
  142. drive.imageName = "test.img"
  143. drive.imageType = .disk
  144. drive.interface = .ide
  145. config.drives.append(drive)
  146. drive = UTMQemuConfigurationDrive(forArchitecture: .x86_64, target: QEMUTarget_x86_64.pc)
  147. drive.imageName = "bios.bin"
  148. drive.imageType = .bios
  149. drive.interface = .none
  150. config.drives.append(drive)
  151. }
  152. }
  153. }
  154. }