remote(client): add connect view

This commit is contained in:
osy 2024-01-24 13:52:05 -08:00
parent 54acd99900
commit 50ef326fa9
4 changed files with 247 additions and 0 deletions

View File

@ -0,0 +1,221 @@
//
// 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
struct UTMRemoteConnectView: View {
@ObservedObject var remoteClientState: UTMRemoteClient.State
@Environment(\.openURL) private var openURL
@EnvironmentObject private var data: UTMData
@State private var selectedServer: UTMRemoteClient.State.Server?
@State private var isAutoConnect: Bool = false
private var idiom: UIUserInterfaceIdiom {
UIDevice.current.userInterfaceIdiom
}
private var remoteClient: UTMRemoteClient {
data.remoteClient
}
var body: some View {
VStack {
HStack {
ProgressView().progressViewStyle(.circular)
Spacer()
Button {
openURL(URL(string: "https://docs.getutm.app/remote/")!)
} label: {
Label("Help", systemImage: "questionmark.circle")
.labelStyle(.iconOnly)
.font(.title2)
}
Button {
} label: {
Label("New Connection", systemImage: "plus")
.labelStyle(.iconOnly)
.font(.title2)
}
}.padding()
List {
Section(header: Text("Saved")) {
ForEach(remoteClientState.savedServers) { server in
Button {
isAutoConnect = true
selectedServer = server
} label: {
Text(server.name)
}.contextMenu {
Button {
isAutoConnect = false
selectedServer = server
} label: {
Label("Edit…", systemImage: "slider.horizontal.3")
}
DestructiveButton("Delete") {
}
}
}.onDelete { indexSet in
}
}
Section(header: Text("Found")) {
ForEach(remoteClientState.foundServers) { server in
Button {
isAutoConnect = true
selectedServer = server
} label: {
Text(server.name)
}
}
}
}.listStyle(.plain)
}.frame(maxWidth: idiom == .pad ? 600 : nil)
.sheet(item: $selectedServer) { server in
ServerConnectView(remoteClientState: remoteClientState, server: server, isAutoConnect: $isAutoConnect)
}
.onAppear {
Task {
await remoteClient.startScanning()
}
}
.onDisappear {
Task {
await remoteClient.stopScanning()
}
}
}
}
private struct ServerConnectView: View {
@ObservedObject var remoteClientState: UTMRemoteClient.State
@State var server: UTMRemoteClient.State.Server
@Binding var isAutoConnect: Bool
@EnvironmentObject private var data: UTMData
@Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
@State private var isConnecting: Bool = false
@State private var isPasswordRequired: Bool = false
@State private var willBeSaved: Bool = true
private var remoteClient: UTMRemoteClient {
data.remoteClient
}
var body: some View {
NavigationView {
Form {
Section {
TextField("Name", text: $server.name)
TextField("Server", text: .constant(server.hostname))
} header: {
Text("Connection")
}
if isPasswordRequired {
Section {
if #available(iOS 15, *) {
FocusedPasswordView(password: $server.password.bound)
} else {
SecureField("Password", text: $server.password.bound)
}
} header: {
Text("Authentication")
}
}
Section {
Toggle("Save Connection", isOn: $willBeSaved)
} header: {
Text("Options")
}
}.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 {
connect()
} label: {
Text("Cancel")
}
} else {
Button {
connect()
} label: {
Text("Connect")
}
}
}
}
}
}
.alert(item: $remoteClientState.alertMessage) { item in
Alert(title: Text(item.message))
}
.onAppear {
if isAutoConnect {
connect()
}
}
}
private func connect() {
Task {
isConnecting = true
do {
try await remoteClient.connect(server, shouldSaveDetails: willBeSaved)
} catch {
if case UTMRemoteClient.ConnectionError.passwordRequired = error {
withAnimation {
isPasswordRequired = true
}
} else {
remoteClientState.showErrorAlert(error.localizedDescription)
}
}
isConnecting = false
}
}
}
@available(iOS 15, *)
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())
}

View File

@ -36,7 +36,11 @@ struct UTMSingleWindowView: View {
if let session = session { if let session = session {
VMWindowView(id: identifier!, isInteractive: isInteractive).environmentObject(session) VMWindowView(id: identifier!, isInteractive: isInteractive).environmentObject(session)
} else if isInteractive { } else if isInteractive {
#if WITH_REMOTE
RemoteContentView(remoteClientState: data.remoteClient.state).environmentObject(data)
#else
ContentView().environmentObject(data) ContentView().environmentObject(data)
#endif
} else { } else {
VStack { VStack {
Text("Waiting for VM to connect to display...") Text("Waiting for VM to connect to display...")
@ -69,6 +73,22 @@ struct UTMSingleWindowView: View {
} }
} }
#if WITH_REMOTE
struct RemoteContentView: View {
@ObservedObject var remoteClientState: UTMRemoteClient.State
var body: some View {
if remoteClientState.isConnected {
ContentView()
.transition(.move(edge: .trailing))
} else {
UTMRemoteConnectView(remoteClientState: remoteClientState)
.transition(.move(edge: .leading))
}
}
}
#endif
struct UTMSingleWindowView_Previews: PreviewProvider { struct UTMSingleWindowView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
UTMSingleWindowView() UTMSingleWindowView()

View File

@ -122,6 +122,8 @@ extension UTMRemoteClient {
@Published var isScanning: Bool = false @Published var isScanning: Bool = false
@Published private(set) var isConnected: Bool = false
@Published var alertMessage: AlertMessage? @Published var alertMessage: AlertMessage?
init() { init() {

View File

@ -884,6 +884,7 @@
CEDF83F9258AE24E0030E4AC /* UTMPasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */; }; CEDF83F9258AE24E0030E4AC /* UTMPasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */; };
CEDF83FA258AE24E0030E4AC /* UTMPasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */; }; CEDF83FA258AE24E0030E4AC /* UTMPasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */; };
CEE06B272B2FC89400A811AE /* UTMServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE06B262B2FC89400A811AE /* UTMServerView.swift */; }; CEE06B272B2FC89400A811AE /* UTMServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE06B262B2FC89400A811AE /* UTMServerView.swift */; };
CEE06B292B30013500A811AE /* UTMRemoteConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE06B282B30013500A811AE /* UTMRemoteConnectView.swift */; };
CEE7E936287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; }; CEE7E936287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; };
CEE7E937287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; }; CEE7E937287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; };
CEE7E938287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; }; CEE7E938287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; };
@ -1991,6 +1992,7 @@
CEE0421024418F2E0001680F /* UTMLegacyQemuConfiguration+Miscellaneous.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UTMLegacyQemuConfiguration+Miscellaneous.h"; sourceTree = "<group>"; }; CEE0421024418F2E0001680F /* UTMLegacyQemuConfiguration+Miscellaneous.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UTMLegacyQemuConfiguration+Miscellaneous.h"; sourceTree = "<group>"; };
CEE0421124418F2E0001680F /* UTMLegacyQemuConfiguration+Miscellaneous.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UTMLegacyQemuConfiguration+Miscellaneous.m"; sourceTree = "<group>"; }; CEE0421124418F2E0001680F /* UTMLegacyQemuConfiguration+Miscellaneous.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UTMLegacyQemuConfiguration+Miscellaneous.m"; sourceTree = "<group>"; };
CEE06B262B2FC89400A811AE /* UTMServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMServerView.swift; sourceTree = "<group>"; }; CEE06B262B2FC89400A811AE /* UTMServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMServerView.swift; sourceTree = "<group>"; };
CEE06B282B30013500A811AE /* UTMRemoteConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteConnectView.swift; sourceTree = "<group>"; };
CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UTMLegacyQemuConfiguration+Constants.m"; sourceTree = "<group>"; }; CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UTMLegacyQemuConfiguration+Constants.m"; sourceTree = "<group>"; };
CEE7E935287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UTMLegacyQemuConfiguration+Constants.h"; sourceTree = "<group>"; }; CEE7E935287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UTMLegacyQemuConfiguration+Constants.h"; sourceTree = "<group>"; };
CEE7ED472A90256100E6B4AB /* VMDisplayMetalViewController+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VMDisplayMetalViewController+Private.h"; sourceTree = "<group>"; }; CEE7ED472A90256100E6B4AB /* VMDisplayMetalViewController+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VMDisplayMetalViewController+Private.h"; sourceTree = "<group>"; };
@ -2639,6 +2641,7 @@
841E58CA28937EE200137A20 /* UTMExternalSceneDelegate.swift */, 841E58CA28937EE200137A20 /* UTMExternalSceneDelegate.swift */,
841E58CD28937FED00137A20 /* UTMSingleWindowView.swift */, 841E58CD28937FED00137A20 /* UTMSingleWindowView.swift */,
842B9F8C28CC58B700031EE7 /* UTMPatches.swift */, 842B9F8C28CC58B700031EE7 /* UTMPatches.swift */,
CEE06B282B30013500A811AE /* UTMRemoteConnectView.swift */,
84CE3DB02904C7A100FF068B /* UTMSettingsView.swift */, 84CE3DB02904C7A100FF068B /* UTMSettingsView.swift */,
CE2D954D24AD4F980059923A /* VMConfigNetworkPortForwardView.swift */, CE2D954D24AD4F980059923A /* VMConfigNetworkPortForwardView.swift */,
84CF5DD2288DCE6400D01721 /* VMDisplayHostedView.swift */, 84CF5DD2288DCE6400D01721 /* VMDisplayHostedView.swift */,
@ -3935,6 +3938,7 @@
CEF7F5982AEEDCC400E34952 /* VMSettingsAddDeviceMenuView.swift in Sources */, CEF7F5982AEEDCC400E34952 /* VMSettingsAddDeviceMenuView.swift in Sources */,
CEF7F5992AEEDCC400E34952 /* VMRemovableDrivesView.swift in Sources */, CEF7F5992AEEDCC400E34952 /* VMRemovableDrivesView.swift in Sources */,
CEF7F59A2AEEDCC400E34952 /* UTMQemuConfigurationDrive.swift in Sources */, CEF7F59A2AEEDCC400E34952 /* UTMQemuConfigurationDrive.swift in Sources */,
CEE06B292B30013500A811AE /* UTMRemoteConnectView.swift in Sources */,
CEF7F59B2AEEDCC400E34952 /* UTMQemuConfigurationSharing.swift in Sources */, CEF7F59B2AEEDCC400E34952 /* UTMQemuConfigurationSharing.swift in Sources */,
CEF7F59C2AEEDCC400E34952 /* ContentView.swift in Sources */, CEF7F59C2AEEDCC400E34952 /* ContentView.swift in Sources */,
CEF7F59D2AEEDCC400E34952 /* VMData.swift in Sources */, CEF7F59D2AEEDCC400E34952 /* VMData.swift in Sources */,