mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 00:27:31 +00:00
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 8a9a05f04e
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
115 lines
4.1 KiB
Swift
115 lines
4.1 KiB
Swift
import SwiftUI
|
|
|
|
struct RootTabs: View {
|
|
@Environment(NodeAppModel.self) private var appModel
|
|
@Environment(VoiceWakeManager.self) private var voiceWake
|
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
@AppStorage(VoiceWakePreferences.enabledKey) private var voiceWakeEnabled: Bool = false
|
|
@State private var selectedTab: Int = 0
|
|
@State private var voiceWakeToastText: String?
|
|
@State private var toastDismissTask: Task<Void, Never>?
|
|
@State private var showGatewayActions: Bool = false
|
|
|
|
var body: some View {
|
|
TabView(selection: self.$selectedTab) {
|
|
ScreenTab()
|
|
.tabItem { Label("Screen", systemImage: "rectangle.and.hand.point.up.left") }
|
|
.tag(0)
|
|
|
|
VoiceTab()
|
|
.tabItem { Label("Voice", systemImage: "mic") }
|
|
.tag(1)
|
|
|
|
SettingsTab()
|
|
.tabItem { Label("Settings", systemImage: "gearshape") }
|
|
.tag(2)
|
|
}
|
|
.overlay(alignment: .topLeading) {
|
|
StatusPill(
|
|
gateway: self.gatewayStatus,
|
|
voiceWakeEnabled: self.voiceWakeEnabled,
|
|
activity: self.statusActivity,
|
|
onTap: {
|
|
if self.gatewayStatus == .connected {
|
|
self.showGatewayActions = true
|
|
} else {
|
|
self.selectedTab = 2
|
|
}
|
|
})
|
|
.padding(.leading, 10)
|
|
.safeAreaPadding(.top, 10)
|
|
}
|
|
.overlay(alignment: .topLeading) {
|
|
if let voiceWakeToastText, !voiceWakeToastText.isEmpty {
|
|
VoiceWakeToast(command: voiceWakeToastText)
|
|
.padding(.leading, 10)
|
|
.safeAreaPadding(.top, 58)
|
|
.transition(.move(edge: .top).combined(with: .opacity))
|
|
}
|
|
}
|
|
.onChange(of: self.voiceWake.lastTriggeredCommand) { _, newValue in
|
|
guard let newValue else { return }
|
|
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { return }
|
|
|
|
self.toastDismissTask?.cancel()
|
|
withAnimation(self.reduceMotion ? .none : .spring(response: 0.25, dampingFraction: 0.85)) {
|
|
self.voiceWakeToastText = trimmed
|
|
}
|
|
|
|
self.toastDismissTask = Task {
|
|
try? await Task.sleep(nanoseconds: 2_300_000_000)
|
|
await MainActor.run {
|
|
withAnimation(self.reduceMotion ? .none : .easeOut(duration: 0.25)) {
|
|
self.voiceWakeToastText = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.onDisappear {
|
|
self.toastDismissTask?.cancel()
|
|
self.toastDismissTask = nil
|
|
}
|
|
.confirmationDialog(
|
|
"Gateway",
|
|
isPresented: self.$showGatewayActions,
|
|
titleVisibility: .visible)
|
|
{
|
|
Button("Disconnect", role: .destructive) {
|
|
self.appModel.disconnectGateway()
|
|
}
|
|
Button("Open Settings") {
|
|
self.selectedTab = 2
|
|
}
|
|
Button("Cancel", role: .cancel) {}
|
|
} message: {
|
|
Text("Disconnect from the gateway?")
|
|
}
|
|
}
|
|
|
|
private var gatewayStatus: StatusPill.GatewayState {
|
|
if self.appModel.gatewayServerName != nil { return .connected }
|
|
|
|
let text = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if text.localizedCaseInsensitiveContains("connecting") ||
|
|
text.localizedCaseInsensitiveContains("reconnecting")
|
|
{
|
|
return .connecting
|
|
}
|
|
|
|
if text.localizedCaseInsensitiveContains("error") {
|
|
return .error
|
|
}
|
|
|
|
return .disconnected
|
|
}
|
|
|
|
private var statusActivity: StatusPill.Activity? {
|
|
StatusActivityBuilder.build(
|
|
appModel: self.appModel,
|
|
voiceWakeEnabled: self.voiceWakeEnabled,
|
|
cameraHUDText: self.appModel.cameraHUDText,
|
|
cameraHUDKind: self.appModel.cameraHUDKind)
|
|
}
|
|
}
|