2
0

VMToolbarDriveMenuView.swift 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. //
  2. // Copyright © 2022 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. struct VMToolbarDriveMenuView: View {
  18. @State var config: UTMQemuConfiguration
  19. @EnvironmentObject private var session: VMSessionState
  20. @State private var isFileImporterShown: Bool = false
  21. @State private var isSelectingShare: Bool = false
  22. @State private var selectedDrive: UTMQemuConfigurationDrive?
  23. @State private var isRefreshRequired: Bool = false
  24. private let noneText = NSLocalizedString("none", comment: "VMToolbarDriveMenuView")
  25. var body: some View {
  26. Menu {
  27. if config.sharing.directoryShareMode == .webdav {
  28. Menu {
  29. Button {
  30. selectedDrive = nil
  31. isSelectingShare = true
  32. isFileImporterShown.toggle()
  33. } label: {
  34. MenuLabel("Change…", systemImage: "folder.badge.person.crop")
  35. }
  36. Button {
  37. Task {
  38. await session.vm.clearSharedDirectory()
  39. }
  40. } label: {
  41. MenuLabel("Clear…", systemImage: "clear")
  42. }
  43. } label: {
  44. let url = session.vm.sharedDirectoryURL
  45. MenuLabel("Shared Directory: \(url?.lastPathComponent ?? noneText)", systemImage: url == nil ? "folder.badge.person.crop" : "folder.fill.badge.person.crop")
  46. }
  47. Divider()
  48. }
  49. ForEach(config.drives) { drive in
  50. if drive.isExternal {
  51. #if !WITH_REMOTE // FIXME: implement remote feature
  52. Menu {
  53. Button {
  54. selectedDrive = drive
  55. isSelectingShare = false
  56. isFileImporterShown.toggle()
  57. } label: {
  58. MenuLabel("Change…", systemImage: "opticaldisc")
  59. }
  60. Button {
  61. ejectDriveImage(for: drive)
  62. } label: {
  63. MenuLabel("Eject…", systemImage: "eject")
  64. }
  65. } label: {
  66. MenuLabel(label(for: drive), systemImage: session.vm.externalImageURL(for: drive) == nil ? "opticaldiscdrive" : "opticaldiscdrive.fill")
  67. }
  68. #else
  69. Button {
  70. } label: {
  71. MenuLabel(label(for: drive), systemImage: session.vm.externalImageURL(for: drive) == nil ? "opticaldiscdrive" : "opticaldiscdrive.fill")
  72. }.disabled(true)
  73. #endif
  74. } else if drive.imageType == .disk || drive.imageType == .cd {
  75. Button {
  76. } label: {
  77. MenuLabel(label(for: drive), systemImage: "internaldrive")
  78. }.disabled(true)
  79. }
  80. }
  81. } label: {
  82. Label("Disk", systemImage: "opticaldisc")
  83. }.fileImporter(isPresented: $isFileImporterShown, allowedContentTypes: isSelectingShare ? [.folder] : [.item]) { result in
  84. switch result {
  85. case .success(let success):
  86. if isSelectingShare {
  87. changeSharedDirectory(to: success)
  88. } else if let drive = selectedDrive {
  89. changeDriveImage(for: drive, with: success)
  90. }
  91. case .failure(let failure):
  92. session.nonfatalError = failure.localizedDescription
  93. }
  94. }
  95. .onChange(of: isRefreshRequired) { _ in
  96. // dummy here since UTMDrive is not observable
  97. // this forces a redraw when we toggle
  98. }
  99. }
  100. private func changeDriveImage(for drive: UTMQemuConfigurationDrive, with imageURL: URL) {
  101. Task.detached(priority: .background) {
  102. do {
  103. try await session.vm.changeMedium(drive, to: imageURL)
  104. Task { @MainActor in
  105. isRefreshRequired.toggle()
  106. }
  107. } catch {
  108. Task { @MainActor in
  109. session.nonfatalError = error.localizedDescription
  110. }
  111. }
  112. }
  113. }
  114. private func changeSharedDirectory(to url: URL) {
  115. Task.detached(priority: .background) {
  116. do {
  117. try await session.vm.changeSharedDirectory(to: url)
  118. Task { @MainActor in
  119. isRefreshRequired.toggle()
  120. }
  121. } catch {
  122. Task { @MainActor in
  123. session.nonfatalError = error.localizedDescription
  124. }
  125. }
  126. }
  127. }
  128. private func ejectDriveImage(for drive: UTMQemuConfigurationDrive) {
  129. Task.detached(priority: .background) {
  130. do {
  131. try await session.vm.eject(drive)
  132. Task { @MainActor in
  133. isRefreshRequired.toggle()
  134. }
  135. } catch {
  136. Task { @MainActor in
  137. session.nonfatalError = error.localizedDescription
  138. }
  139. }
  140. }
  141. }
  142. private func label(for drive: UTMQemuConfigurationDrive) -> String {
  143. let imageURL = session.vm.externalImageURL(for: drive) ?? drive.imageURL
  144. return String.localizedStringWithFormat(NSLocalizedString("%@ (%@): %@", comment: "VMToolbarDriveMenuView"),
  145. drive.imageType.prettyValue,
  146. drive.interface.prettyValue,
  147. imageURL?.lastPathComponent ?? noneText)
  148. }
  149. }
  150. struct VMToolbarDriveMenuView_Previews: PreviewProvider {
  151. @StateObject static var config = UTMQemuConfiguration()
  152. static var previews: some View {
  153. VMToolbarDriveMenuView(config: config)
  154. }
  155. }