123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- //
- // Copyright © 2020 osy. All rights reserved.
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- //
- import SwiftUI
- import UniformTypeIdentifiers
- #if os(iOS)
- import IQKeyboardManagerSwift
- #endif
- #if WITH_QEMU_TCI
- let productName = "UTM SE"
- #elseif WITH_REMOTE
- let productName = "UTM Remote"
- #else
- let productName = "UTM"
- #endif
- struct ContentView: View {
- @State private var editMode = false
- @EnvironmentObject private var data: UTMData
- @StateObject private var releaseHelper = UTMReleaseHelper()
- @State private var newPopupPresented = false
- @State private var openSheetPresented = false
- @Environment(\.openURL) var openURL
-
- var body: some View {
- VMNavigationListView()
- .overlay(data.showSettingsModal ? AnyView(EmptyView()) : AnyView(BusyOverlay()))
- #if os(macOS) || os(visionOS)
- .frame(minWidth: 800, idealWidth: 1200, minHeight: 600, idealHeight: 800)
- #endif
- .disabled(data.busy && !data.showNewVMSheet && !data.showSettingsModal)
- .sheet(isPresented: $releaseHelper.isReleaseNotesShown, onDismiss: {
- releaseHelper.closeReleaseNotes()
- }, content: {
- VMReleaseNotesView(helper: releaseHelper).padding()
- })
- .onReceive(NSNotification.ShowReleaseNotes) { _ in
- Task {
- await releaseHelper.fetchReleaseNotes(force: true)
- }
- }
- .onOpenURL(perform: handleURL)
- .handlesExternalEvents(preferring: ["*"], allowing: ["*"])
- .onReceive(NSNotification.NewVirtualMachine) { _ in
- data.newVM()
- }.onReceive(NSNotification.OpenVirtualMachine) { _ in
- // VMNavigationListView also gets this notification and closes the wizard sheet
- openSheetPresented = false
- // FIXME: SwiftUI bug on iOS requires this wait
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
- openSheetPresented = true
- }
- }.fileImporter(isPresented: $openSheetPresented, allowedContentTypes: [.UTM, .UTMextension], allowsMultipleSelection: true, onCompletion: selectImportedUTM)
- .onDrop(of: [.fileURL], delegate: self)
- .onAppear {
- Task {
- await data.listRefresh()
- }
- Task {
- await releaseHelper.fetchReleaseNotes()
- }
- #if os(macOS)
- NSWindow.allowsAutomaticWindowTabbing = false
- #else
- data.triggeriOSNetworkAccessPrompt()
- #if !os(visionOS)
- IQKeyboardManager.shared.enable = true
- #endif
- #if WITH_JIT
- if !Main.jitAvailable {
- data.busyWorkAsync {
- let jitStreamerAttach = UserDefaults.standard.bool(forKey: "JitStreamerAttach")
- if #available(iOS 15, *), jitStreamerAttach {
- try await data.jitStreamerAttach()
- return
- }
- #if canImport(AltKit)
- if await data.isAltServerCompatible {
- try await data.startAltJIT()
- return
- }
- #endif
- // ignore error when we are running on a HV only build
- if !UTMCapabilities.current.contains(.hasHypervisorSupport) {
- 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")
- }
- }
- }
- #endif
- #endif
- }
- }
-
- private func handleURL(url: URL) {
- data.busyWorkAsync {
- if url.isFileURL {
- try await importUTM(url: url)
- } else if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
- let scheme = components.scheme,
- scheme.lowercased() == "utm" {
- try await handleUTMURL(with: components)
- }
- }
- }
-
- private func importUTM(url: URL) async throws {
- guard url.isFileURL else {
- return // ignore
- }
- try await data.importUTM(from: url)
- }
-
- private func selectImportedUTM(result: Result<[URL], Error>) {
- data.busyWorkAsync {
- let urls = try result.get()
- for url in urls {
- try await data.importUTM(from: url)
- }
- }
- }
-
- @MainActor private func handleUTMURL(with components: URLComponents) async throws {
- func findVM() -> VMData? {
- if let vmName = components.queryItems?.first(where: { $0.name == "name" })?.value {
- return data.virtualMachines.first(where: { $0.detailsTitleLabel == vmName })
- } else {
- return nil
- }
- }
-
- if let action = components.host {
- switch action {
- case "start":
- if let vm = findVM(), vm.state == .stopped {
- data.run(vm: vm)
- }
- break
- case "stop":
- if let vm = findVM(), vm.state == .started {
- try await vm.wrapped!.stop(usingMethod: .force)
- data.stop(vm: vm)
- }
- break
- case "restart":
- if let vm = findVM(), vm.state == .started {
- try await vm.wrapped!.restart()
- }
- break
- case "pause":
- if let vm = findVM(), vm.state == .started {
- let shouldSaveOnPause: Bool
- if let vm = vm.wrapped as? (any UTMSpiceVirtualMachine) {
- shouldSaveOnPause = !vm.isRunningAsDisposible
- } else {
- shouldSaveOnPause = true
- }
- try await vm.wrapped!.pause()
- if shouldSaveOnPause {
- try? await vm.wrapped!.saveSnapshot(name: nil)
- }
- }
- case "resume":
- if let vm = findVM(), vm.state == .paused {
- try await vm.wrapped!.resume()
- }
- break
- case "sendText":
- if let vm = findVM(), vm.state == .started {
- data.automationSendText(to: vm, urlComponents: components)
- }
- break
- case "click":
- if let vm = findVM(), vm.state == .started {
- data.automationSendMouse(to: vm, urlComponents: components)
- }
- break
- case "downloadVM":
- await data.downloadUTMZip(from: components)
- break
- default:
- return
- }
- }
- }
- }
- extension ContentView: DropDelegate {
- func validateDrop(info: DropInfo) -> Bool {
- !urlsFrom(info: info).isEmpty
- }
-
- func performDrop(info: DropInfo) -> Bool {
- let urls = urlsFrom(info: info)
- data.busyWorkAsync {
- for url in urls {
-
- try await data.importUTM(from: url)
- }
- }
- return true
- }
-
- private func urlsFrom(info: DropInfo) -> [URL] {
- let providers = info.itemProviders(for: [.fileURL])
- var validURLs: [URL] = []
- let group = DispatchGroup()
- providers.forEach { provider in
- group.enter()
- _ = provider.loadObject(ofClass: URL.self) { url, _ in
- if url?.pathExtension == "utm" {
- validURLs.append(url!)
- }
- group.leave()
- }
- }
-
- group.wait()
- return validURLs
- }
- }
- struct ContentView_Previews: PreviewProvider {
- static var previews: some View {
- ContentView()
- }
- }
|