2023-11-16 16:53:44 +00:00
|
|
|
//
|
|
|
|
// ConnectDesktopView.swift
|
|
|
|
// SimpleX (iOS)
|
|
|
|
//
|
|
|
|
// Created by Evgeny on 13/10/2023.
|
|
|
|
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
import SimpleXChat
|
|
|
|
import CodeScanner
|
|
|
|
|
|
|
|
struct ConnectDesktopView: View {
|
|
|
|
@EnvironmentObject var m: ChatModel
|
|
|
|
@Environment(\.dismiss) var dismiss: DismissAction
|
|
|
|
var viaSettings = false
|
|
|
|
@AppStorage(DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS) private var deviceName = UIDevice.current.name
|
|
|
|
@AppStorage(DEFAULT_CONFIRM_REMOTE_SESSIONS) private var confirmRemoteSessions = false
|
|
|
|
@AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) private var connectRemoteViaMulticast = false
|
|
|
|
@AppStorage(DEFAULT_OFFER_REMOTE_MULTICAST) private var offerRemoteMulticast = true
|
|
|
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
|
|
|
@State private var sessionAddress: String = ""
|
|
|
|
@State private var remoteCtrls: [RemoteCtrlInfo] = []
|
|
|
|
@State private var alert: ConnectDesktopAlert?
|
|
|
|
|
|
|
|
private enum ConnectDesktopAlert: Identifiable {
|
|
|
|
case unlinkDesktop(rc: RemoteCtrlInfo)
|
|
|
|
case disconnectDesktop(action: UserDisconnectAction)
|
|
|
|
case badInvitationError
|
|
|
|
case badVersionError(version: String?)
|
|
|
|
case desktopDisconnectedError
|
|
|
|
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
|
|
|
|
|
|
|
var id: String {
|
|
|
|
switch self {
|
|
|
|
case let .unlinkDesktop(rc): "unlinkDesktop \(rc.remoteCtrlId)"
|
|
|
|
case let .disconnectDesktop(action): "disconnectDecktop \(action)"
|
|
|
|
case .badInvitationError: "badInvitationError"
|
|
|
|
case let .badVersionError(v): "badVersionError \(v ?? "")"
|
|
|
|
case .desktopDisconnectedError: "desktopDisconnectedError"
|
|
|
|
case let .error(title, _): "error \(title)"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private enum UserDisconnectAction: String {
|
|
|
|
case back
|
|
|
|
case dismiss // TODO dismiss settings after confirmation
|
|
|
|
}
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
if viaSettings {
|
|
|
|
viewBody
|
|
|
|
.modifier(BackButton(label: "Back") {
|
|
|
|
if m.activeRemoteCtrl {
|
|
|
|
alert = .disconnectDesktop(action: .back)
|
|
|
|
} else {
|
|
|
|
dismiss()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
NavigationView {
|
|
|
|
viewBody
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var viewBody: some View {
|
|
|
|
Group {
|
|
|
|
if let session = m.remoteCtrlSession {
|
|
|
|
switch session.sessionState {
|
|
|
|
case .starting: connectingDesktopView(session, nil)
|
|
|
|
case let .connecting(rc_): connectingDesktopView(session, rc_)
|
|
|
|
case let .pendingConfirmation(rc_, sessCode):
|
|
|
|
if confirmRemoteSessions || rc_ == nil {
|
|
|
|
verifySessionView(session, rc_, sessCode)
|
|
|
|
} else {
|
|
|
|
connectingDesktopView(session, rc_).onAppear {
|
|
|
|
verifyDesktopSessionCode(sessCode)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case let .connected(rc, _): activeSessionView(session, rc)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
connectDesktopView()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.onAppear {
|
|
|
|
setDeviceName(deviceName)
|
|
|
|
updateRemoteCtrls()
|
|
|
|
}
|
|
|
|
.onDisappear {
|
|
|
|
if m.remoteCtrlSession != nil {
|
|
|
|
disconnectDesktop()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.onChange(of: deviceName) {
|
|
|
|
setDeviceName($0)
|
|
|
|
}
|
|
|
|
.onChange(of: m.activeRemoteCtrl) {
|
|
|
|
UIApplication.shared.isIdleTimerDisabled = $0
|
|
|
|
}
|
|
|
|
.alert(item: $alert) { a in
|
|
|
|
switch a {
|
|
|
|
case let .unlinkDesktop(rc):
|
|
|
|
Alert(
|
|
|
|
title: Text("Unlink desktop?"),
|
|
|
|
primaryButton: .destructive(Text("Unlink")) {
|
|
|
|
unlinkDesktop(rc)
|
|
|
|
},
|
|
|
|
secondaryButton: .cancel()
|
|
|
|
)
|
|
|
|
case let .disconnectDesktop(action):
|
|
|
|
Alert(
|
|
|
|
title: Text("Disconnect desktop?"),
|
|
|
|
primaryButton: .destructive(Text("Disconnect")) {
|
|
|
|
disconnectDesktop(action)
|
|
|
|
},
|
|
|
|
secondaryButton: .cancel()
|
|
|
|
)
|
|
|
|
case .badInvitationError:
|
|
|
|
Alert(title: Text("Bad desktop address"))
|
|
|
|
case let .badVersionError(v):
|
|
|
|
Alert(
|
|
|
|
title: Text("Incompatible version"),
|
|
|
|
message: Text("Desktop app version \(v ?? "") is not compatible with this app.")
|
|
|
|
)
|
|
|
|
case .desktopDisconnectedError:
|
|
|
|
Alert(title: Text("Connection terminated"))
|
|
|
|
case let .error(title, error):
|
|
|
|
Alert(title: Text(title), message: Text(error))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.interactiveDismissDisabled(m.activeRemoteCtrl)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func connectDesktopView() -> some View {
|
|
|
|
List {
|
|
|
|
Section("This device name") {
|
|
|
|
devicesView()
|
|
|
|
}
|
|
|
|
scanDesctopAddressView()
|
|
|
|
if developerTools {
|
|
|
|
desktopAddressView()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.navigationTitle("Connect to desktop")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
|
|
|
|
List {
|
|
|
|
Section("Connecting to desktop") {
|
|
|
|
ctrlDeviceNameText(session, rc)
|
|
|
|
ctrlDeviceVersionText(session)
|
|
|
|
}
|
|
|
|
|
|
|
|
if let sessCode = session.sessionCode {
|
|
|
|
Section("Session code") {
|
|
|
|
sessionCodeText(sessCode)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Section {
|
|
|
|
disconnectButton()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.navigationTitle("Connecting to desktop")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func verifySessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?, _ sessCode: String) -> some View {
|
|
|
|
List {
|
|
|
|
Section("Connected to desktop") {
|
|
|
|
ctrlDeviceNameText(session, rc)
|
|
|
|
ctrlDeviceVersionText(session)
|
|
|
|
}
|
|
|
|
|
|
|
|
Section("Verify code with desktop") {
|
|
|
|
sessionCodeText(sessCode)
|
|
|
|
Button {
|
|
|
|
verifyDesktopSessionCode(sessCode)
|
|
|
|
} label: {
|
|
|
|
Label("Confirm", systemImage: "checkmark")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Section {
|
|
|
|
disconnectButton()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.navigationTitle("Verify connection")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
|
|
|
|
var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo.deviceName)
|
|
|
|
if (rc == nil) {
|
|
|
|
t = t + Text(" ") + Text("(new)").italic()
|
|
|
|
}
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
|
|
|
|
private func ctrlDeviceVersionText(_ session: RemoteCtrlSession) -> Text {
|
|
|
|
let v = session.ctrlAppInfo.appVersionRange.maxVersion
|
|
|
|
var t = Text("v\(v)")
|
|
|
|
if v != session.appVersion {
|
|
|
|
t = t + Text(" ") + Text("(this device v\(session.appVersion))").italic()
|
|
|
|
}
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
|
|
|
|
private func activeSessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo) -> some View {
|
|
|
|
List {
|
|
|
|
Section("Connected desktop") {
|
|
|
|
Text(rc.deviceViewName)
|
|
|
|
ctrlDeviceVersionText(session)
|
|
|
|
}
|
|
|
|
|
|
|
|
if let sessCode = session.sessionCode {
|
|
|
|
Section("Session code") {
|
|
|
|
sessionCodeText(sessCode)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Section {
|
|
|
|
disconnectButton()
|
|
|
|
} footer: {
|
|
|
|
// This is specific to iOS
|
|
|
|
Text("Keep the app open to use it from desktop")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.navigationTitle("Connected to desktop")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func sessionCodeText(_ code: String) -> some View {
|
|
|
|
Text(code.prefix(23))
|
|
|
|
}
|
|
|
|
|
|
|
|
private func devicesView() -> some View {
|
|
|
|
Group {
|
|
|
|
TextField("Enter this device name…", text: $deviceName)
|
|
|
|
if !remoteCtrls.isEmpty {
|
|
|
|
NavigationLink {
|
|
|
|
linkedDesktopsView()
|
|
|
|
} label: {
|
|
|
|
Text("Linked desktops")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func scanDesctopAddressView() -> some View {
|
|
|
|
Section("Scan QR code from desktop") {
|
|
|
|
CodeScannerView(codeTypes: [.qr], completion: processDesktopQRCode)
|
|
|
|
.aspectRatio(1, contentMode: .fit)
|
|
|
|
.cornerRadius(12)
|
|
|
|
.listRowBackground(Color.clear)
|
|
|
|
.listRowSeparator(.hidden)
|
|
|
|
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
|
|
|
.padding(.horizontal)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func desktopAddressView() -> some View {
|
|
|
|
Section("Desktop address") {
|
|
|
|
if sessionAddress.isEmpty {
|
|
|
|
Button {
|
|
|
|
sessionAddress = UIPasteboard.general.string ?? ""
|
|
|
|
} label: {
|
|
|
|
Label("Paste desktop address", systemImage: "doc.plaintext")
|
|
|
|
}
|
|
|
|
.disabled(!UIPasteboard.general.hasStrings)
|
|
|
|
} else {
|
|
|
|
HStack {
|
|
|
|
Text(sessionAddress).lineLimit(1)
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "multiply.circle.fill")
|
|
|
|
.foregroundColor(.secondary)
|
|
|
|
.onTapGesture { sessionAddress = "" }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Button {
|
|
|
|
connectDesktopAddress(sessionAddress)
|
|
|
|
} label: {
|
|
|
|
Label("Connect to desktop", systemImage: "rectangle.connected.to.line.below")
|
|
|
|
}
|
|
|
|
.disabled(sessionAddress.isEmpty)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func linkedDesktopsView() -> some View {
|
|
|
|
List {
|
|
|
|
Section("Desktop devices") {
|
|
|
|
ForEach(remoteCtrls, id: \.remoteCtrlId) { rc in
|
|
|
|
remoteCtrlView(rc)
|
|
|
|
}
|
|
|
|
.onDelete { indexSet in
|
|
|
|
if let i = indexSet.first, i < remoteCtrls.count {
|
|
|
|
alert = .unlinkDesktop(rc: remoteCtrls[i])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Section("Linked desktop options") {
|
|
|
|
Toggle("Verify connections", isOn: $confirmRemoteSessions)
|
|
|
|
Toggle("Discover on network", isOn: $connectRemoteViaMulticast).disabled(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.navigationTitle("Linked desktops")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func remoteCtrlView(_ rc: RemoteCtrlInfo) -> some View {
|
|
|
|
Text(rc.deviceViewName)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func setDeviceName(_ name: String) {
|
|
|
|
do {
|
|
|
|
try setLocalDeviceName(deviceName)
|
|
|
|
} catch let e {
|
|
|
|
errorAlert(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateRemoteCtrls() {
|
|
|
|
do {
|
|
|
|
remoteCtrls = try listRemoteCtrls()
|
|
|
|
} catch let e {
|
|
|
|
errorAlert(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func processDesktopQRCode(_ resp: Result<ScanResult, ScanError>) {
|
|
|
|
switch resp {
|
|
|
|
case let .success(r): connectDesktopAddress(r.string)
|
|
|
|
case let .failure(e): errorAlert(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func connectDesktopAddress(_ addr: String) {
|
|
|
|
Task {
|
|
|
|
do {
|
|
|
|
let (rc_, ctrlAppInfo, v) = try await connectRemoteCtrl(desktopAddress: addr)
|
|
|
|
await MainActor.run {
|
|
|
|
sessionAddress = ""
|
|
|
|
m.remoteCtrlSession = RemoteCtrlSession(
|
|
|
|
ctrlAppInfo: ctrlAppInfo,
|
|
|
|
appVersion: v,
|
|
|
|
sessionState: .connecting(remoteCtrl_: rc_)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} catch let e {
|
|
|
|
await MainActor.run {
|
|
|
|
switch e as? ChatResponse {
|
|
|
|
case .chatCmdError(_, .errorRemoteCtrl(.badInvitation)): alert = .badInvitationError
|
|
|
|
case .chatCmdError(_, .error(.commandError)): alert = .badInvitationError
|
|
|
|
case let .chatCmdError(_, .errorRemoteCtrl(.badVersion(v))): alert = .badVersionError(version: v)
|
|
|
|
case .chatCmdError(_, .errorAgent(.RCP(.version))): alert = .badVersionError(version: nil)
|
|
|
|
case .chatCmdError(_, .errorAgent(.RCP(.ctrlAuth))): alert = .desktopDisconnectedError
|
|
|
|
default: errorAlert(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func verifyDesktopSessionCode(_ sessCode: String) {
|
|
|
|
Task {
|
|
|
|
do {
|
|
|
|
let rc = try await verifyRemoteCtrlSession(sessCode)
|
|
|
|
await MainActor.run {
|
|
|
|
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(.connected(remoteCtrl: rc, sessionCode: sessCode))
|
|
|
|
}
|
|
|
|
await MainActor.run {
|
|
|
|
updateRemoteCtrls()
|
|
|
|
}
|
|
|
|
} catch let error {
|
|
|
|
await MainActor.run {
|
|
|
|
errorAlert(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func disconnectButton() -> some View {
|
|
|
|
Button {
|
2023-11-22 22:10:41 +00:00
|
|
|
disconnectDesktop(.dismiss)
|
2023-11-16 16:53:44 +00:00
|
|
|
} label: {
|
|
|
|
Label("Disconnect", systemImage: "multiply")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func disconnectDesktop(_ action: UserDisconnectAction? = nil) {
|
|
|
|
Task {
|
|
|
|
do {
|
|
|
|
try await stopRemoteCtrl()
|
|
|
|
await MainActor.run {
|
|
|
|
switchToLocalSession()
|
|
|
|
switch action {
|
|
|
|
case .back: dismiss()
|
|
|
|
case .dismiss: dismiss()
|
|
|
|
case .none: ()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch let e {
|
|
|
|
await MainActor.run {
|
|
|
|
errorAlert(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func unlinkDesktop(_ rc: RemoteCtrlInfo) {
|
|
|
|
Task {
|
|
|
|
do {
|
|
|
|
try await deleteRemoteCtrl(rc.remoteCtrlId)
|
|
|
|
await MainActor.run {
|
|
|
|
remoteCtrls.removeAll(where: { $0.remoteCtrlId == rc.remoteCtrlId })
|
|
|
|
}
|
|
|
|
} catch let e {
|
|
|
|
await MainActor.run {
|
|
|
|
errorAlert(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func errorAlert(_ error: Error) {
|
|
|
|
let a = getErrorAlert(error, "Error")
|
|
|
|
alert = .error(title: a.title, error: a.message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#Preview {
|
|
|
|
ConnectDesktopView()
|
|
|
|
}
|