ContentView.swift 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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. import TipKit
  22. // on visionOS, there is no text to show more than UTM
  23. #if WITH_QEMU_TCI && !os(visionOS)
  24. let productName = "UTM SE"
  25. #elseif WITH_REMOTE && !os(visionOS)
  26. let productName = "UTM Remote"
  27. #else
  28. let productName = "UTM"
  29. #endif
  30. struct ContentView: View {
  31. @State private var editMode = false
  32. @EnvironmentObject private var data: UTMData
  33. @StateObject private var releaseHelper = UTMReleaseHelper()
  34. @State private var openSheetPresented = false
  35. @Environment(\.openURL) var openURL
  36. @AppStorage("ServerAutostart") private var isServerAutostart: Bool = false
  37. var body: some View {
  38. VMNavigationListView()
  39. .overlay(data.showSettingsModal ? AnyView(EmptyView()) : AnyView(BusyOverlay()))
  40. #if os(macOS) || os(visionOS)
  41. .frame(minWidth: 800, idealWidth: 1200, minHeight: 600, idealHeight: 800)
  42. #endif
  43. .disabled(data.busy && !data.showNewVMSheet && !data.showSettingsModal)
  44. .sheet(isPresented: $releaseHelper.isReleaseNotesShown, onDismiss: {
  45. releaseHelper.closeReleaseNotes()
  46. if #available(iOS 17, macOS 14, *) {
  47. UTMTipCreateVM.isVMListEmpty = data.virtualMachines.count == 0
  48. }
  49. }, content: {
  50. VMReleaseNotesView(helper: releaseHelper).padding()
  51. })
  52. .onReceive(NSNotification.ShowReleaseNotes) { _ in
  53. Task {
  54. await releaseHelper.fetchReleaseNotes(force: true)
  55. }
  56. }
  57. .onOpenURL(perform: handleURL)
  58. .handlesExternalEvents(preferring: ["*"], allowing: ["*"])
  59. .onReceive(NSNotification.NewVirtualMachine) { _ in
  60. data.newVM()
  61. }.onReceive(NSNotification.OpenVirtualMachine) { _ in
  62. // VMNavigationListView also gets this notification and closes the wizard sheet
  63. openSheetPresented = false
  64. // FIXME: SwiftUI bug on iOS requires this wait
  65. DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
  66. openSheetPresented = true
  67. }
  68. }.fileImporter(isPresented: $openSheetPresented, allowedContentTypes: [.UTM, .UTMextension], allowsMultipleSelection: true, onCompletion: selectImportedUTM)
  69. .onReceive(NSNotification.InstallGuestTools) { notification in
  70. guard let vm = notification.object as? any UTMVirtualMachine else {
  71. logger.error("InstallGuestTools notification but no VM object is provided!")
  72. return
  73. }
  74. Task {
  75. data.busyWorkAsync {
  76. try await data.mountSupportTools(for: vm)
  77. }
  78. }
  79. }
  80. .onDrop(of: [.fileURL], delegate: self)
  81. .onAppear {
  82. Task {
  83. await data.listRefresh()
  84. await releaseHelper.fetchReleaseNotes()
  85. if #available(iOS 17, macOS 14, *) {
  86. if !releaseHelper.isReleaseNotesShown {
  87. UTMTipCreateVM.isVMListEmpty = data.virtualMachines.count == 0
  88. UTMTipDonate.timesLaunched += 1
  89. }
  90. }
  91. #if os(macOS)
  92. if isServerAutostart {
  93. await data.remoteServer.start()
  94. }
  95. #endif
  96. }
  97. #if os(macOS)
  98. NSWindow.allowsAutomaticWindowTabbing = false
  99. #else
  100. data.triggeriOSNetworkAccessPrompt()
  101. #if !os(visionOS)
  102. IQKeyboardManager.shared.enable = true
  103. #endif
  104. #if WITH_JIT
  105. if !Main.jitAvailable {
  106. data.busyWorkAsync {
  107. let jitStreamerAttach = UserDefaults.standard.bool(forKey: "JitStreamerAttach")
  108. if #available(iOS 15, *), jitStreamerAttach {
  109. try await data.jitStreamerAttach()
  110. return
  111. }
  112. #if canImport(AltKit)
  113. if await data.isAltServerCompatible {
  114. try await data.startAltJIT()
  115. return
  116. }
  117. #endif
  118. // ignore error when we are running on a HV only build
  119. if !UTMCapabilities.current.contains(.hasHypervisorSupport) {
  120. 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")
  121. }
  122. }
  123. }
  124. #endif
  125. #endif
  126. }
  127. #if WITH_SERVER
  128. .onChange(of: isServerAutostart) { newValue in
  129. if newValue {
  130. Task {
  131. if isServerAutostart && !data.remoteServer.state.isServerActive {
  132. await data.remoteServer.start()
  133. }
  134. }
  135. }
  136. }
  137. #endif
  138. }
  139. private func handleURL(url: URL) {
  140. if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
  141. components.scheme?.lowercased() == "utm",
  142. components.host == "downloadVM",
  143. let urlParameter = components.queryItems?.first(where: { $0.name == "url" })?.value,
  144. let url = URL(string: urlParameter) {
  145. if data.alertItem == nil {
  146. data.showNewVMSheet = false
  147. data.alertItem = .downloadUrl(url)
  148. }
  149. } else if url.isFileURL {
  150. data.busyWorkAsync {
  151. try await importUTM(url: url)
  152. }
  153. }
  154. }
  155. private func importUTM(url: URL) async throws {
  156. guard url.isFileURL else {
  157. return // ignore
  158. }
  159. try await data.importUTM(from: url)
  160. }
  161. private func selectImportedUTM(result: Result<[URL], Error>) {
  162. data.busyWorkAsync {
  163. let urls = try result.get()
  164. for url in urls {
  165. try await data.importUTM(from: url)
  166. }
  167. }
  168. }
  169. }
  170. extension ContentView: DropDelegate {
  171. func validateDrop(info: DropInfo) -> Bool {
  172. !urlsFrom(info: info).isEmpty
  173. }
  174. func performDrop(info: DropInfo) -> Bool {
  175. let urls = urlsFrom(info: info)
  176. data.busyWorkAsync {
  177. for url in urls {
  178. try await data.importUTM(from: url)
  179. }
  180. }
  181. return true
  182. }
  183. private func urlsFrom(info: DropInfo) -> [URL] {
  184. let providers = info.itemProviders(for: [.fileURL])
  185. var validURLs: [URL] = []
  186. let group = DispatchGroup()
  187. providers.forEach { provider in
  188. group.enter()
  189. _ = provider.loadObject(ofClass: URL.self) { url, _ in
  190. if url?.pathExtension == "utm" {
  191. validURLs.append(url!)
  192. }
  193. group.leave()
  194. }
  195. }
  196. group.wait()
  197. return validURLs
  198. }
  199. }
  200. struct ContentView_Previews: PreviewProvider {
  201. static var previews: some View {
  202. ContentView()
  203. }
  204. }