2
0

VMConfigAppleDriveDetailsView.swift 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. //
  2. // Copyright © 2021 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. private let bytesInMib: Int64 = 1024 * 1024
  18. private let mibInGib: Int = 1024
  19. struct VMConfigAppleDriveDetailsView: View {
  20. private enum ConfirmItem: Identifiable {
  21. case resize(URL)
  22. var id: Int {
  23. switch self {
  24. case .resize(_): return 3
  25. }
  26. }
  27. }
  28. @Binding var config: UTMAppleConfigurationDrive
  29. @Binding var requestDriveDelete: UTMAppleConfigurationDrive?
  30. @EnvironmentObject private var data: UTMData
  31. @State private var confirmAlert: ConfirmItem?
  32. @State private var isResizePopoverShown: Bool = false
  33. @State private var proposedSizeMib: Int = 0
  34. var body: some View {
  35. Form {
  36. Toggle(isOn: $config.isExternal, label: {
  37. Text("Removable Drive")
  38. }).disabled(true)
  39. TextField("Name", text: .constant(config.imageURL?.lastPathComponent ?? NSLocalizedString("(New Drive)", comment: "VMConfigAppleDriveDetailsView")))
  40. .disabled(true)
  41. Toggle("Read Only?", isOn: $config.isReadOnly)
  42. if #available(macOS 14, *), !config.isExternal {
  43. Toggle(isOn: $config.isNvme,
  44. label: {
  45. Text("Use NVMe Interface")
  46. }).help("If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors.")
  47. }
  48. DefaultTextField("Size", text: .constant(config.sizeString)).disabled(true)
  49. HStack {
  50. if #unavailable(macOS 12) {
  51. Button {
  52. requestDriveDelete = config
  53. } label: {
  54. Label("Delete Drive", systemImage: "externaldrive.badge.minus")
  55. .foregroundColor(.red)
  56. }.help("Delete this drive.")
  57. }
  58. if #available(macOS 14, *), let imageUrl = config.imageURL, FileManager.default.fileExists(atPath: imageUrl.path) {
  59. Button {
  60. isResizePopoverShown.toggle()
  61. } label: {
  62. Label("Resize…", systemImage: "arrowtriangle.left.and.line.vertical.and.arrowtriangle.right")
  63. }.help("Increase the size of the disk image.")
  64. .popover(isPresented: $isResizePopoverShown) {
  65. ResizePopoverView(imageURL: imageUrl, proposedSizeMib: $proposedSizeMib) {
  66. confirmAlert = .resize(imageUrl)
  67. }.padding()
  68. .frame(minHeight: 120)
  69. }
  70. }
  71. }.alert(item: $confirmAlert) { item in
  72. switch item {
  73. case .resize(let imageURL):
  74. Alert(title: Text("Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to \(proposedSizeMib / mibInGib) GiB?"), primaryButton: .destructive(Text("Resize")) {
  75. resizeDrive(for: imageURL, sizeInMib: proposedSizeMib)
  76. }, secondaryButton: .cancel())
  77. }
  78. }
  79. }
  80. }
  81. private func resizeDrive(for driveUrl: URL, sizeInMib: Int) {
  82. if #available(macOS 14, *) {
  83. data.busyWorkAsync {
  84. try await data.resizeAppleDrive(for: driveUrl, sizeInMib: sizeInMib)
  85. }
  86. }
  87. }
  88. }
  89. @available(macOS 14, *)
  90. private struct ResizePopoverView: View {
  91. let imageURL: URL
  92. @Binding var proposedSizeMib: Int
  93. let onConfirm: () -> Void
  94. @EnvironmentObject private var data: UTMData
  95. @State private var currentSize: Int64?
  96. @State private var imageFormat: String?
  97. @Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
  98. private var sizeString: String? {
  99. if let currentSize = currentSize {
  100. return ByteCountFormatter.string(fromByteCount: currentSize, countStyle: .binary)
  101. } else {
  102. return nil
  103. }
  104. }
  105. private var minSizeMib: Int {
  106. Int((currentSize! + bytesInMib - 1) / bytesInMib)
  107. }
  108. var body: some View {
  109. VStack {
  110. if let sizeString = sizeString {
  111. if let imageFormat = imageFormat {
  112. Text("Image format: \(imageFormat)")
  113. }
  114. Text("Minimum size: \(sizeString)")
  115. Form {
  116. SizeTextField($proposedSizeMib, minSizeMib: minSizeMib)
  117. Button("Resize") {
  118. if proposedSizeMib > minSizeMib {
  119. onConfirm()
  120. }
  121. presentationMode.wrappedValue.dismiss()
  122. }
  123. }
  124. } else {
  125. ProgressView("Calculating current size...")
  126. }
  127. }.onAppear {
  128. Task { @MainActor in
  129. (imageFormat, currentSize) = data.appleDriveInfo(for: imageURL)
  130. proposedSizeMib = minSizeMib
  131. }
  132. }
  133. }
  134. }
  135. struct VMConfigAppleDriveDetailsView_Previews: PreviewProvider {
  136. static var previews: some View {
  137. VMConfigAppleDriveDetailsView(config: .constant(UTMAppleConfigurationDrive(newSize: 100)), requestDriveDelete: .constant(nil))
  138. }
  139. }