Kaynağa Gözat

remote(client): add connect view

osy 1 yıl önce
ebeveyn
işleme
50ef326fa9

+ 221 - 0
Platform/iOS/UTMRemoteConnectView.swift

@@ -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())
+}

+ 20 - 0
Platform/iOS/UTMSingleWindowView.swift

@@ -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()

+ 2 - 0
Remote/UTMRemoteClient.swift

@@ -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() {

+ 4 - 0
UTM.xcodeproj/project.pbxproj

@@ -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 */,