ContentView.swift 7.2 KB

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