Add native security key registration, reference import, and signing on macOS and iOS. Store references in Vault metadata and route authentication through connection, Assistant, MCP, and agent forwarding flows. Preserve signing cancellation and actionable authentication errors for security keys and Secure Enclave. Pin the released libssh2 forks as nested submodules and refresh package resolution.
150 lines
6.3 KiB
Swift
150 lines
6.3 KiB
Swift
import SwiftUI
|
|
import TermindPresentation
|
|
|
|
/// A window retains its feature stacks while the system adapts tabs into a sidebar.
|
|
struct MobileRootView: View {
|
|
let app: MobileAppModel
|
|
private let features: MobileFeatureRegistry
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
@Environment(\.horizontalSizeClass) private var sizeClass
|
|
@State private var scene: MobileSceneModel
|
|
private let pendingQuickAction: MobileHomeScreenQuickAction?
|
|
private let consumePendingQuickAction: () -> MobileHomeScreenQuickAction?
|
|
|
|
init(
|
|
app: MobileAppModel,
|
|
scene: MobileSceneModel,
|
|
features: MobileFeatureRegistry = .standard(),
|
|
pendingQuickAction: MobileHomeScreenQuickAction? = nil,
|
|
consumePendingQuickAction: @escaping () -> MobileHomeScreenQuickAction? = { nil }
|
|
) {
|
|
self.app = app
|
|
self.features = features
|
|
self.pendingQuickAction = pendingQuickAction
|
|
self.consumePendingQuickAction = consumePendingQuickAction
|
|
_scene = State(initialValue: scene)
|
|
}
|
|
|
|
var body: some View {
|
|
TabView(selection: selection) {
|
|
ForEach(features.registrations) { feature in
|
|
Tab(value: feature.id) {
|
|
NavigationStack {
|
|
feature.makeView(app: app, scene: scene)
|
|
}
|
|
.mobileToolbarEnvironment()
|
|
} label: {
|
|
Label(feature.title, systemImage: feature.symbol)
|
|
}
|
|
.badge(unreadCount(for: feature.id))
|
|
}
|
|
}
|
|
.tabViewStyle(.sidebarAdaptable)
|
|
.modifier(InitialNotificationAuthorization(model: app.notificationSettings, service: app.notifications))
|
|
.onChange(of: scene.visibleAssistantConversationID, initial: true) { app.refreshAssistantReadState() }
|
|
.environment(\.singleLineInputAdapter, { AnyView(MobileSingleLineFieldInput()) })
|
|
.sheet(isPresented: Binding(get: { app.pro.isPaywallPresented }, set: { app.pro.isPaywallPresented = $0 })) {
|
|
MobileProView(pro: app.pro)
|
|
}
|
|
.sheet(item: $scene.renamingWorkspace) { descriptor in
|
|
MobileWorkspaceRenameView(descriptor: descriptor) { title in
|
|
scene.workspaceSession.renameWorkspace(descriptor.id, to: title)
|
|
}
|
|
}
|
|
.sheet(
|
|
isPresented: Binding(
|
|
get: { scene.agentForwarding.current != nil },
|
|
set: { if !$0 { scene.agentForwarding.cancelAll() } }
|
|
)
|
|
) {
|
|
if let challenge = scene.agentForwarding.current {
|
|
MobileAgentForwardingSheet(interaction: scene.agentForwarding, request: challenge)
|
|
.id(challenge.id)
|
|
}
|
|
}
|
|
.onDisappear { scene.agentForwarding.isActive = false }
|
|
.onChange(of: scenePhase, initial: true) { _, phase in
|
|
scene.agentForwarding.isActive = phase != .background
|
|
app.sceneChanged(scene.id, phase: phase, workspaceSessionID: scene.workspaceSession.id)
|
|
MobileHomeScreenQuickActions.refresh(app: app)
|
|
}
|
|
.onChange(of: app.connectionHistory.revision, initial: true) { _, _ in
|
|
MobileHomeScreenQuickActions.refresh(app: app)
|
|
}
|
|
.onChange(of: app.library.hosts) { _, _ in
|
|
MobileHomeScreenQuickActions.refresh(app: app)
|
|
}
|
|
.onChange(of: app.library.identities) { _, _ in
|
|
MobileHomeScreenQuickActions.refresh(app: app)
|
|
}
|
|
.onChange(of: pendingQuickAction, initial: true) { _, action in
|
|
guard action != nil, let action = consumePendingQuickAction() else { return }
|
|
_ = scene.perform(action)
|
|
}
|
|
.onChange(of: app.pro.hasProAccess) { _, _ in app.reconcileSync() }
|
|
.onChange(of: scene.workspaceSession.workspaces.map(\.id)) { _, workspaceIDs in
|
|
scene.reconcileWorkspaceIDs(workspaceIDs)
|
|
}
|
|
.onDisappear { app.removeScene(scene.id) }
|
|
.onOpenURL { url in
|
|
MobileWorkspaceSceneRouting.reconcile(app: app, activeSessionID: scene.workspaceSession.id)
|
|
scene.open(url)
|
|
}
|
|
.focusedSceneValue(\.mobileScene, scene)
|
|
.alert(
|
|
AppStrings.Workspace.openTelnetConnectionConfirmation,
|
|
isPresented: Binding(
|
|
get: { scene.pendingExternalConnection != nil },
|
|
set: { if !$0 { scene.pendingExternalConnection = nil } }
|
|
)
|
|
) {
|
|
Button(AppStrings.Common.cancel.swiftUIKey, role: .cancel) { scene.pendingExternalConnection = nil }
|
|
Button(AppStrings.Common.continue.swiftUIKey) { scene.confirmExternalConnection() }
|
|
} message: {
|
|
Text(AppStrings.Workspace.advisoryTelnetPassword)
|
|
}
|
|
.alert(
|
|
scene.isWorkspaceUnavailable
|
|
? String(localized: AppStrings.Workspace.workspaceUnavailable)
|
|
: String(localized: AppStrings.Common.operationFailed),
|
|
isPresented: Binding(
|
|
get: { scene.isWorkspaceUnavailable || app.error != nil },
|
|
set: {
|
|
if !$0 {
|
|
scene.isWorkspaceUnavailable = false
|
|
app.error = nil
|
|
}
|
|
})
|
|
) {
|
|
Button(AppStrings.Common.ok.swiftUIKey, role: .cancel) {
|
|
scene.isWorkspaceUnavailable = false
|
|
app.error = nil
|
|
}
|
|
} message: {
|
|
Text(
|
|
scene.isWorkspaceUnavailable
|
|
? String(
|
|
localized:
|
|
AppStrings.Workspace.unavailableWorkspaceExplanation
|
|
) : app.error ?? "")
|
|
}
|
|
.modifier(MobileAppReviewPresenter(app: app, scene: scene, scenePhase: scenePhase))
|
|
.environment(\.mobileLayout, MobileLayout(sizeClass: sizeClass))
|
|
}
|
|
|
|
private var selection: Binding<MobileDestination> {
|
|
Binding(
|
|
get: { scene.selectedDestination ?? .vault },
|
|
set: { scene.selectedDestination = $0 }
|
|
)
|
|
}
|
|
|
|
private func unreadCount(for destination: MobileDestination) -> Int {
|
|
switch destination {
|
|
case .workspaces: scene.workspaceSession.unreadCompletionCount
|
|
case .assistant: app.unreadAssistantNotificationCount
|
|
default: 0
|
|
}
|
|
}
|
|
}
|