ContentView.swift 8.3 KB

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