ContentView.swift 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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. import UniformTypeIdentifiers
  18. #if os(iOS)
  19. import IQKeyboardManagerSwift
  20. #endif
  21. #if WITH_QEMU_TCI
  22. let productName = "UTM SE"
  23. #elseif WITH_REMOTE
  24. let productName = "UTM Remote"
  25. #else
  26. let productName = "UTM"
  27. #endif
  28. struct ContentView: View {
  29. @State private var editMode = false
  30. @EnvironmentObject private var data: UTMData
  31. @StateObject private var releaseHelper = UTMReleaseHelper()
  32. @State private var newPopupPresented = false
  33. @State private var openSheetPresented = false
  34. @Environment(\.openURL) var openURL
  35. var body: some View {
  36. VMNavigationListView()
  37. .overlay(data.showSettingsModal ? AnyView(EmptyView()) : AnyView(BusyOverlay()))
  38. #if os(macOS) || os(visionOS)
  39. .frame(minWidth: 800, idealWidth: 1200, minHeight: 600, idealHeight: 800)
  40. #endif
  41. .disabled(data.busy && !data.showNewVMSheet && !data.showSettingsModal)
  42. .sheet(isPresented: $releaseHelper.isReleaseNotesShown, onDismiss: {
  43. releaseHelper.closeReleaseNotes()
  44. }, content: {
  45. VMReleaseNotesView(helper: releaseHelper).padding()
  46. })
  47. .onReceive(NSNotification.ShowReleaseNotes) { _ in
  48. Task {
  49. await releaseHelper.fetchReleaseNotes(force: true)
  50. }
  51. }
  52. .onOpenURL(perform: handleURL)
  53. .handlesExternalEvents(preferring: ["*"], allowing: ["*"])
  54. .onReceive(NSNotification.NewVirtualMachine) { _ in
  55. data.newVM()
  56. }.onReceive(NSNotification.OpenVirtualMachine) { _ in
  57. // VMNavigationListView also gets this notification and closes the wizard sheet
  58. openSheetPresented = false
  59. // FIXME: SwiftUI bug on iOS requires this wait
  60. DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
  61. openSheetPresented = true
  62. }
  63. }.fileImporter(isPresented: $openSheetPresented, allowedContentTypes: [.UTM, .UTMextension], allowsMultipleSelection: true, onCompletion: selectImportedUTM)
  64. .onDrop(of: [.fileURL], delegate: self)
  65. .onAppear {
  66. Task {
  67. await data.listRefresh()
  68. }
  69. Task {
  70. await releaseHelper.fetchReleaseNotes()
  71. }
  72. #if os(macOS)
  73. NSWindow.allowsAutomaticWindowTabbing = false
  74. #else
  75. data.triggeriOSNetworkAccessPrompt()
  76. #if !os(visionOS)
  77. IQKeyboardManager.shared.enable = true
  78. #endif
  79. #if WITH_JIT
  80. if !Main.jitAvailable {
  81. data.busyWorkAsync {
  82. let jitStreamerAttach = UserDefaults.standard.bool(forKey: "JitStreamerAttach")
  83. if #available(iOS 15, *), jitStreamerAttach {
  84. try await data.jitStreamerAttach()
  85. return
  86. }
  87. #if canImport(AltKit)
  88. if await data.isAltServerCompatible {
  89. try await data.startAltJIT()
  90. return
  91. }
  92. #endif
  93. // ignore error when we are running on a HV only build
  94. if !UTMCapabilities.current.contains(.hasHypervisorSupport) {
  95. throw NSLocalizedString("Your version of iOS does not support running VMs while unmodified. You must either run UTM while jailbroken or with a remote debugger attached. See https://getutm.app/install/ for more details.", comment: "ContentView")
  96. }
  97. }
  98. }
  99. #endif
  100. #endif
  101. }
  102. }
  103. private func handleURL(url: URL) {
  104. data.busyWorkAsync {
  105. if url.isFileURL {
  106. try await importUTM(url: url)
  107. } else if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
  108. let scheme = components.scheme,
  109. scheme.lowercased() == "utm" {
  110. try await handleUTMURL(with: components)
  111. }
  112. }
  113. }
  114. private func importUTM(url: URL) async throws {
  115. guard url.isFileURL else {
  116. return // ignore
  117. }
  118. try await data.importUTM(from: url)
  119. }
  120. private func selectImportedUTM(result: Result<[URL], Error>) {
  121. data.busyWorkAsync {
  122. let urls = try result.get()
  123. for url in urls {
  124. try await data.importUTM(from: url)
  125. }
  126. }
  127. }
  128. @MainActor private func handleUTMURL(with components: URLComponents) async throws {
  129. func findVM() -> VMData? {
  130. if let vmName = components.queryItems?.first(where: { $0.name == "name" })?.value {
  131. return data.virtualMachines.first(where: { $0.detailsTitleLabel == vmName })
  132. } else {
  133. return nil
  134. }
  135. }
  136. if let action = components.host {
  137. switch action {
  138. case "start":
  139. if let vm = findVM(), vm.state == .stopped {
  140. data.run(vm: vm)
  141. }
  142. break
  143. case "stop":
  144. if let vm = findVM(), vm.state == .started {
  145. try await vm.wrapped!.stop(usingMethod: .force)
  146. data.stop(vm: vm)
  147. }
  148. break
  149. case "restart":
  150. if let vm = findVM(), vm.state == .started {
  151. try await vm.wrapped!.restart()
  152. }
  153. break
  154. case "pause":
  155. if let vm = findVM(), vm.state == .started {
  156. let shouldSaveOnPause: Bool
  157. if let vm = vm.wrapped as? (any UTMSpiceVirtualMachine) {
  158. shouldSaveOnPause = !vm.isRunningAsDisposible
  159. } else {
  160. shouldSaveOnPause = true
  161. }
  162. try await vm.wrapped!.pause()
  163. if shouldSaveOnPause {
  164. try? await vm.wrapped!.saveSnapshot(name: nil)
  165. }
  166. }
  167. case "resume":
  168. if let vm = findVM(), vm.state == .paused {
  169. try await vm.wrapped!.resume()
  170. }
  171. break
  172. case "sendText":
  173. if let vm = findVM(), vm.state == .started {
  174. data.automationSendText(to: vm, urlComponents: components)
  175. }
  176. break
  177. case "click":
  178. if let vm = findVM(), vm.state == .started {
  179. data.automationSendMouse(to: vm, urlComponents: components)
  180. }
  181. break
  182. case "downloadVM":
  183. await data.downloadUTMZip(from: components)
  184. break
  185. default:
  186. return
  187. }
  188. }
  189. }
  190. }
  191. extension ContentView: DropDelegate {
  192. func validateDrop(info: DropInfo) -> Bool {
  193. !urlsFrom(info: info).isEmpty
  194. }
  195. func performDrop(info: DropInfo) -> Bool {
  196. let urls = urlsFrom(info: info)
  197. data.busyWorkAsync {
  198. for url in urls {
  199. try await data.importUTM(from: url)
  200. }
  201. }
  202. return true
  203. }
  204. private func urlsFrom(info: DropInfo) -> [URL] {
  205. let providers = info.itemProviders(for: [.fileURL])
  206. var validURLs: [URL] = []
  207. let group = DispatchGroup()
  208. providers.forEach { provider in
  209. group.enter()
  210. _ = provider.loadObject(ofClass: URL.self) { url, _ in
  211. if url?.pathExtension == "utm" {
  212. validURLs.append(url!)
  213. }
  214. group.leave()
  215. }
  216. }
  217. group.wait()
  218. return validURLs
  219. }
  220. }
  221. struct ContentView_Previews: PreviewProvider {
  222. static var previews: some View {
  223. ContentView()
  224. }
  225. }