mirror of
https://github.com/utmapp/UTM.git
synced 2026-10-12 02:24:01 +00:00
Now that the deployment targets are iOS 15 and macOS 12, every guard, fallback and workaround for an older release is dead code that only obscures what the supported path is. Remove them: - Drop the `@available` attributes for iOS 15, macOS 11 and macOS 12, and unwrap the matching `#available`/`#unavailable` guards in Swift and the `@available`/`API_AVAILABLE`/`NS_AVAILABLE_IOS` ones in Objective-C. - Delete the legacy-only views and helpers left unreachable by that: NumberTextFieldOld, VMConfigNetworkPortForwardLegacyView, VMContextMenuModifier.bodyBigSur, the nonbrokenSheet, uniqued, customBadge, borderedList and legacySettingsToolbar shims, the .unsupported BootloaderSelection case, and the iOS 14 sacrificial version Section in the QEMU settings list. - jb_has_cs_execseg_allow_unsigned() has returned false for every host since iOS 14.4, so drop it along with the is_device_A12_or_newer() helper that only it used. The entitlement parser no longer needs to copy the blob either, since that copy existed only so the psychic paper needle could be blanked in place. - VMDisplayTerminalViewController tracked the keyboard frame by hand for iOS 14. `useAutoLayout` is a constant that cannot be overridden, so nothing ever registered the notification observers and only the keyboardLayoutGuide path is reachable. - VMConfigQEMUView's additionalArguments is now referenced on iOS only, which leaves its macOS branch, and the macOS 11 ForEach crash workaround inside it, unreachable. - Raise the Cydia package floor and the postinst/prerm helpers to match the new minimum. Two of the removed guards dropped an error path rather than just a branch. The shared directory `featureNotSupported` throw covered VZVirtioFileSystemDeviceConfiguration and friends being macOS 12+, which no longer applies to any supported host. The macOS guest `platformUnsupported` throw when decoding a boot config on Intel only fired on macOS 11; the Apple Silicon requirement it really guards is enforced unconditionally in UTMAppleConfigurationSystem.fillVZConfiguration, which both the start and the save path go through. Assisted-by: Claude:claude-opus-5
307 lines
11 KiB
Swift
307 lines
11 KiB
Swift
//
|
|
// Copyright © 2023 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
|
|
|
|
private let kTimeoutSeconds: UInt64 = 60
|
|
|
|
struct UTMRemoteConnectView: View {
|
|
@ObservedObject var remoteClientState: UTMRemoteClient.State
|
|
@Environment(\.openURL) private var openURL
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
@EnvironmentObject private var data: UTMRemoteData
|
|
@State private var selectedServer: UTMRemoteClient.State.SavedServer?
|
|
@State private var isAutoConnect: Bool = false
|
|
|
|
private var remoteClient: UTMRemoteClient {
|
|
data.remoteClient
|
|
}
|
|
|
|
var body: some View {
|
|
VStack {
|
|
HStack {
|
|
if remoteClientState.isScanning {
|
|
ProgressView().progressViewStyle(.circular)
|
|
}
|
|
Spacer()
|
|
Text("Select a UTM Server")
|
|
.font(.headline)
|
|
Spacer()
|
|
Button {
|
|
openURL(URL(string: "https://docs.getutm.app/remote/")!)
|
|
} label: {
|
|
Label("Help", systemImage: "questionmark.circle")
|
|
.labelStyle(.iconOnly)
|
|
.font(.title2)
|
|
}
|
|
Button {
|
|
selectedServer = .init()
|
|
} label: {
|
|
Label("New Connection", systemImage: "plus")
|
|
.labelStyle(.iconOnly)
|
|
.font(.title2)
|
|
}
|
|
}.padding()
|
|
List {
|
|
if remoteClientState.savedServers.count > 0 {
|
|
Section(header: Text("Saved")) {
|
|
ForEach(remoteClientState.savedServers) { server in
|
|
Button {
|
|
isAutoConnect = true
|
|
selectedServer = server
|
|
} label: {
|
|
MacDeviceLabel(server.name.isEmpty ? server.hostname : server.name, device: .init(model: server.model))
|
|
}.disabled(!server.isAvailable)
|
|
.contextMenu {
|
|
Button {
|
|
isAutoConnect = false
|
|
selectedServer = server
|
|
} label: {
|
|
Label("Edit…", systemImage: "slider.horizontal.3")
|
|
}
|
|
DestructiveButton("Delete") {
|
|
remoteClientState.delete(server: server)
|
|
Task {
|
|
await remoteClient.refresh()
|
|
}
|
|
}
|
|
}
|
|
}.onDelete { indexSet in
|
|
remoteClientState.savedServers.remove(atOffsets: indexSet)
|
|
Task {
|
|
await remoteClient.refresh()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Section(header: Text("Discovered"), footer: helpText) {
|
|
ForEach(remoteClientState.foundServers) { server in
|
|
Button {
|
|
isAutoConnect = true
|
|
selectedServer = UTMRemoteClient.State.SavedServer(from: server)
|
|
} label: {
|
|
MacDeviceLabel(server.name, device: .init(model: server.model))
|
|
}
|
|
}
|
|
}
|
|
}.listStyle(.insetGrouped)
|
|
}.alert(item: $remoteClientState.alertMessage) { item in
|
|
Alert(title: Text(item.message), primaryButton: .default(Text("Open Settings")) {
|
|
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
|
|
}, secondaryButton: .cancel(Text("Retry")) {
|
|
if !remoteClientState.isScanning {
|
|
Task {
|
|
await remoteClient.startScanning()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
.sheet(item: $selectedServer) { server in
|
|
ServerConnectView(remoteClientState: remoteClientState, server: server, isAutoConnect: $isAutoConnect)
|
|
}
|
|
.onAppear {
|
|
Task {
|
|
await remoteClient.startScanning()
|
|
}
|
|
}
|
|
.onDisappear {
|
|
Task {
|
|
await remoteClient.stopScanning()
|
|
}
|
|
}
|
|
.onChange(of: scenePhase) { newValue in
|
|
if newValue == .active && !remoteClientState.isScanning {
|
|
Task {
|
|
await remoteClient.startScanning()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var helpText: some View {
|
|
if remoteClientState.foundServers.isEmpty {
|
|
Text("Make sure the latest version of UTM is running on your Mac and UTM Server is enabled. You can download UTM from the Mac App Store.")
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ServerConnectView: View {
|
|
@ObservedObject var remoteClientState: UTMRemoteClient.State
|
|
@State var server: UTMRemoteClient.State.SavedServer
|
|
@Binding var isAutoConnect: Bool
|
|
|
|
@EnvironmentObject private var data: UTMRemoteData
|
|
@Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
|
|
|
|
@State private var connectionTask: Task<Void, Error>?
|
|
private var isConnecting: Bool {
|
|
connectionTask != nil
|
|
}
|
|
@State private var isPasswordRequired: Bool = false
|
|
@State private var isTrustButton: Bool = false
|
|
|
|
private var remoteClient: UTMRemoteClient {
|
|
data.remoteClient
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
Form {
|
|
Section {
|
|
TextField("", text: $server.name, prompt: Text("Name (optional)"))
|
|
} header: {
|
|
Text("Name")
|
|
}
|
|
Section {
|
|
if server.endpoint != nil {
|
|
Text(server.hostname)
|
|
} else {
|
|
TextField("", text: $server.hostname, prompt: Text("Hostname or IP address"))
|
|
.keyboardType(.asciiCapable)
|
|
.autocorrectionDisabled()
|
|
.textInputAutocapitalization(.never)
|
|
TextField("", value: $server.port, format: .number.grouping(.never), prompt: Text("Port"))
|
|
.keyboardType(.decimalPad)
|
|
}
|
|
} header: {
|
|
Text("Host")
|
|
}
|
|
let fingerprint = (server.fingerprint ^ remoteClient.fingerprint).hexString()
|
|
if !fingerprint.isEmpty {
|
|
Section {
|
|
if #available(iOS 16.4, *) {
|
|
Text(fingerprint).monospaced()
|
|
} else {
|
|
Text(fingerprint)
|
|
}
|
|
} header: {
|
|
Text("Fingerprint")
|
|
}
|
|
}
|
|
if isPasswordRequired {
|
|
Section {
|
|
FocusedPasswordView(password: $server.password.bound)
|
|
Toggle("Save Password", isOn: $server.shouldSavePassword)
|
|
} header: {
|
|
Text("Password")
|
|
}
|
|
}
|
|
}.disabled(isConnecting)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
Button {
|
|
presentationMode.wrappedValue.dismiss()
|
|
} label: {
|
|
Text("Close")
|
|
}.disabled(isConnecting)
|
|
}
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
HStack {
|
|
if isConnecting {
|
|
ProgressView().progressViewStyle(.circular)
|
|
Button {
|
|
connectionTask?.cancel()
|
|
} label: {
|
|
Text("Cancel")
|
|
}
|
|
} else {
|
|
Button {
|
|
connect()
|
|
} label: {
|
|
if isTrustButton {
|
|
Text("Trust")
|
|
} else {
|
|
Text("Connect")
|
|
}
|
|
}.disabled(server.hostname.isEmpty || !server.isAvailable)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
// if we have an existing password, assume it should be saved
|
|
if server.password?.isEmpty == false {
|
|
server.shouldSavePassword = true
|
|
}
|
|
if isAutoConnect {
|
|
connect()
|
|
}
|
|
}
|
|
.alert(item: $remoteClientState.alertMessage) { item in
|
|
Alert(title: Text(item.message))
|
|
}
|
|
}
|
|
|
|
private func connect() {
|
|
guard connectionTask == nil else {
|
|
return
|
|
}
|
|
connectionTask = Task {
|
|
let timeoutTask = Task {
|
|
try await Task.sleep(nanoseconds: kTimeoutSeconds * NSEC_PER_SEC)
|
|
connectionTask?.cancel()
|
|
remoteClientState.showErrorAlert(NSLocalizedString("Timed out trying to connect.", comment: "UTMRemoteConnectView"))
|
|
}
|
|
await _connect()
|
|
timeoutTask.cancel()
|
|
connectionTask = nil
|
|
}
|
|
}
|
|
|
|
private func _connect() async {
|
|
do {
|
|
try await remoteClient.connect(server)
|
|
} catch {
|
|
if case UTMRemoteClient.ConnectionError.passwordRequired = error {
|
|
withAnimation {
|
|
isPasswordRequired = true
|
|
isTrustButton = false
|
|
}
|
|
} else if case UTMRemoteClient.ConnectionError.fingerprintUntrusted(let fingerprint) = error, server.fingerprint.isEmpty {
|
|
withAnimation {
|
|
server.fingerprint = fingerprint
|
|
isTrustButton = true
|
|
}
|
|
remoteClientState.showErrorAlert(error.localizedDescription)
|
|
} else if error is CancellationError {
|
|
// ignore it
|
|
} else {
|
|
remoteClientState.showErrorAlert(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct FocusedPasswordView: View {
|
|
@Binding var password: String
|
|
|
|
@FocusState private var isFocused: Bool
|
|
|
|
var body: some View {
|
|
SecureField("Password", text: $password)
|
|
.focused($isFocused)
|
|
.onAppear {
|
|
isFocused = true
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
UTMRemoteConnectView(remoteClientState: .init())
|
|
}
|