remote(client): add connect view
This commit is contained in:
parent
54acd99900
commit
50ef326fa9
|
@ -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())
|
||||
}
|
|
@ -36,7 +36,11 @@ struct UTMSingleWindowView: View {
|
|||
if let session = session {
|
||||
VMWindowView(id: identifier!, isInteractive: isInteractive).environmentObject(session)
|
||||
} else if isInteractive {
|
||||
#if WITH_REMOTE
|
||||
RemoteContentView(remoteClientState: data.remoteClient.state).environmentObject(data)
|
||||
#else
|
||||
ContentView().environmentObject(data)
|
||||
#endif
|
||||
} else {
|
||||
VStack {
|
||||
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 {
|
||||
static var previews: some View {
|
||||
UTMSingleWindowView()
|
||||
|
|
|
@ -122,6 +122,8 @@ extension UTMRemoteClient {
|
|||
|
||||
@Published var isScanning: Bool = false
|
||||
|
||||
@Published private(set) var isConnected: Bool = false
|
||||
|
||||
@Published var alertMessage: AlertMessage?
|
||||
|
||||
init() {
|
||||
|
|
|
@ -884,6 +884,7 @@
|
|||
CEDF83F9258AE24E0030E4AC /* 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 */; };
|
||||
CEE06B292B30013500A811AE /* UTMRemoteConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE06B282B30013500A811AE /* UTMRemoteConnectView.swift */; };
|
||||
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 */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -2639,6 +2641,7 @@
|
|||
841E58CA28937EE200137A20 /* UTMExternalSceneDelegate.swift */,
|
||||
841E58CD28937FED00137A20 /* UTMSingleWindowView.swift */,
|
||||
842B9F8C28CC58B700031EE7 /* UTMPatches.swift */,
|
||||
CEE06B282B30013500A811AE /* UTMRemoteConnectView.swift */,
|
||||
84CE3DB02904C7A100FF068B /* UTMSettingsView.swift */,
|
||||
CE2D954D24AD4F980059923A /* VMConfigNetworkPortForwardView.swift */,
|
||||
84CF5DD2288DCE6400D01721 /* VMDisplayHostedView.swift */,
|
||||
|
@ -3935,6 +3938,7 @@
|
|||
CEF7F5982AEEDCC400E34952 /* VMSettingsAddDeviceMenuView.swift in Sources */,
|
||||
CEF7F5992AEEDCC400E34952 /* VMRemovableDrivesView.swift in Sources */,
|
||||
CEF7F59A2AEEDCC400E34952 /* UTMQemuConfigurationDrive.swift in Sources */,
|
||||
CEE06B292B30013500A811AE /* UTMRemoteConnectView.swift in Sources */,
|
||||
CEF7F59B2AEEDCC400E34952 /* UTMQemuConfigurationSharing.swift in Sources */,
|
||||
CEF7F59C2AEEDCC400E34952 /* ContentView.swift in Sources */,
|
||||
CEF7F59D2AEEDCC400E34952 /* VMData.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue