mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
224 lines
9.2 KiB
Swift
224 lines
9.2 KiB
Swift
import SwiftUI
|
|
|
|
struct HomeToolbar: View {
|
|
var gateway: StatusPill.GatewayState
|
|
var voiceWakeEnabled: Bool
|
|
var activity: StatusPill.Activity?
|
|
var brighten: Bool
|
|
var talkButtonEnabled: Bool
|
|
var talkActive: Bool
|
|
var talkTint: Color
|
|
var onStatusTap: () -> Void
|
|
var onChatTap: () -> Void
|
|
var onTalkTap: () -> Void
|
|
var onSettingsTap: () -> Void
|
|
|
|
@Environment(\.colorSchemeContrast) private var contrast
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
Rectangle()
|
|
.fill(.white.opacity(self.contrast == .increased ? 0.46 : (self.brighten ? 0.18 : 0.12)))
|
|
.frame(height: self.contrast == .increased ? 1.0 : 0.6)
|
|
.allowsHitTesting(false)
|
|
|
|
HStack(spacing: 12) {
|
|
HomeToolbarStatusButton(
|
|
gateway: self.gateway,
|
|
voiceWakeEnabled: self.voiceWakeEnabled,
|
|
activity: self.activity,
|
|
brighten: self.brighten,
|
|
onTap: self.onStatusTap)
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
HStack(spacing: 8) {
|
|
HomeToolbarActionButton(
|
|
systemImage: "text.bubble.fill",
|
|
accessibilityLabel: "Chat",
|
|
brighten: self.brighten,
|
|
action: self.onChatTap)
|
|
|
|
if self.talkButtonEnabled {
|
|
HomeToolbarActionButton(
|
|
systemImage: self.talkActive ? "waveform.circle.fill" : "waveform.circle",
|
|
accessibilityLabel: self.talkActive ? "Talk Mode On" : "Talk Mode Off",
|
|
brighten: self.brighten,
|
|
tint: self.talkTint,
|
|
isActive: self.talkActive,
|
|
action: self.onTalkTap)
|
|
}
|
|
|
|
HomeToolbarActionButton(
|
|
systemImage: "gearshape.fill",
|
|
accessibilityLabel: "Settings",
|
|
brighten: self.brighten,
|
|
action: self.onSettingsTap)
|
|
}
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.top, 10)
|
|
.padding(.bottom, 8)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.background(.ultraThinMaterial)
|
|
.overlay(alignment: .top) {
|
|
LinearGradient(
|
|
colors: [
|
|
.white.opacity(self.brighten ? 0.10 : 0.06),
|
|
.clear,
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom)
|
|
.allowsHitTesting(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct HomeToolbarStatusButton: View {
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
@Environment(\.colorSchemeContrast) private var contrast
|
|
|
|
var gateway: StatusPill.GatewayState
|
|
var voiceWakeEnabled: Bool
|
|
var activity: StatusPill.Activity?
|
|
var brighten: Bool
|
|
var onTap: () -> Void
|
|
|
|
@State private var pulse: Bool = false
|
|
|
|
var body: some View {
|
|
Button(action: self.onTap) {
|
|
HStack(spacing: 8) {
|
|
HStack(spacing: 6) {
|
|
Circle()
|
|
.fill(self.gateway.color)
|
|
.frame(width: 8, height: 8)
|
|
.scaleEffect(
|
|
self.gateway == .connecting && !self.reduceMotion
|
|
? (self.pulse ? 1.15 : 0.85)
|
|
: 1.0
|
|
)
|
|
.opacity(self.gateway == .connecting && !self.reduceMotion ? (self.pulse ? 1.0 : 0.6) : 1.0)
|
|
|
|
Text(self.gateway.title)
|
|
.font(.footnote.weight(.semibold))
|
|
.foregroundStyle(.primary)
|
|
.lineLimit(1)
|
|
}
|
|
|
|
if let activity {
|
|
Image(systemName: activity.systemImage)
|
|
.font(.footnote.weight(.semibold))
|
|
.foregroundStyle(activity.tint ?? .primary)
|
|
.transition(.opacity.combined(with: .move(edge: .top)))
|
|
} else {
|
|
Image(systemName: self.voiceWakeEnabled ? "mic.fill" : "mic.slash")
|
|
.font(.footnote.weight(.semibold))
|
|
.foregroundStyle(self.voiceWakeEnabled ? .primary : .secondary)
|
|
.transition(.opacity.combined(with: .move(edge: .top)))
|
|
}
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 8)
|
|
.background {
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.fill(Color.black.opacity(self.brighten ? 0.12 : 0.18))
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.strokeBorder(
|
|
.white.opacity(self.contrast == .increased ? 0.46 : (self.brighten ? 0.22 : 0.16)),
|
|
lineWidth: self.contrast == .increased ? 1.0 : 0.6)
|
|
}
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel("Connection Status")
|
|
.accessibilityValue(self.accessibilityValue)
|
|
.accessibilityHint(self.gateway == .connected ? "Double tap for gateway actions" : "Double tap to open settings")
|
|
.onAppear { self.updatePulse(for: self.gateway, scenePhase: self.scenePhase, reduceMotion: self.reduceMotion) }
|
|
.onDisappear { self.pulse = false }
|
|
.onChange(of: self.gateway) { _, newValue in
|
|
self.updatePulse(for: newValue, scenePhase: self.scenePhase, reduceMotion: self.reduceMotion)
|
|
}
|
|
.onChange(of: self.scenePhase) { _, newValue in
|
|
self.updatePulse(for: self.gateway, scenePhase: newValue, reduceMotion: self.reduceMotion)
|
|
}
|
|
.onChange(of: self.reduceMotion) { _, newValue in
|
|
self.updatePulse(for: self.gateway, scenePhase: self.scenePhase, reduceMotion: newValue)
|
|
}
|
|
.animation(.easeInOut(duration: 0.18), value: self.activity?.title)
|
|
}
|
|
|
|
private var accessibilityValue: String {
|
|
if let activity {
|
|
return "\(self.gateway.title), \(activity.title)"
|
|
}
|
|
return "\(self.gateway.title), Voice Wake \(self.voiceWakeEnabled ? "enabled" : "disabled")"
|
|
}
|
|
|
|
private func updatePulse(for gateway: StatusPill.GatewayState, scenePhase: ScenePhase, reduceMotion: Bool) {
|
|
guard gateway == .connecting, scenePhase == .active, !reduceMotion else {
|
|
withAnimation(reduceMotion ? .none : .easeOut(duration: 0.2)) { self.pulse = false }
|
|
return
|
|
}
|
|
|
|
guard !self.pulse else { return }
|
|
withAnimation(.easeInOut(duration: 0.9).repeatForever(autoreverses: true)) {
|
|
self.pulse = true
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct HomeToolbarActionButton: View {
|
|
@Environment(\.colorSchemeContrast) private var contrast
|
|
|
|
let systemImage: String
|
|
let accessibilityLabel: String
|
|
let brighten: Bool
|
|
var tint: Color?
|
|
var isActive: Bool = false
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: self.action) {
|
|
Image(systemName: self.systemImage)
|
|
.font(.system(size: 16, weight: .semibold))
|
|
.foregroundStyle(self.isActive ? (self.tint ?? .primary) : .primary)
|
|
.frame(width: 40, height: 40)
|
|
.background {
|
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
.fill(Color.black.opacity(self.brighten ? 0.12 : 0.18))
|
|
.overlay {
|
|
if let tint {
|
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [
|
|
tint.opacity(self.isActive ? 0.22 : 0.14),
|
|
tint.opacity(self.isActive ? 0.08 : 0.04),
|
|
.clear,
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing))
|
|
.blendMode(.overlay)
|
|
}
|
|
}
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
.strokeBorder(
|
|
(self.tint ?? .white).opacity(
|
|
self.isActive
|
|
? 0.34
|
|
: (self.contrast == .increased ? 0.4 : (self.brighten ? 0.22 : 0.16))
|
|
),
|
|
lineWidth: self.contrast == .increased ? 1.0 : (self.isActive ? 0.8 : 0.6))
|
|
}
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel(self.accessibilityLabel)
|
|
}
|
|
}
|