Files
mobile-ios/Rosetta/Features/Calls/CallActionButtons.swift

153 lines
4.4 KiB
Swift

import SwiftUI
// MARK: - Call Action Buttons
/// Telegram-style call control buttons. Adapts layout based on call phase.
struct CallActionButtonsView: View {
@ObservedObject var callManager: CallManager
private var state: CallUiState {
callManager.uiState
}
var body: some View {
if state.phase == .incoming {
incomingButtons
} else {
activeButtons
}
}
// MARK: - Incoming: Decline + Accept
private var incomingButtons: some View {
HStack {
callButton(
title: "Decline",
icon: "phone.down.fill",
background: RosettaColors.error,
foreground: .white
) {
callManager.declineIncomingCall()
}
Spacer()
callButton(
title: "Accept",
icon: "phone.fill",
background: RosettaColors.success,
foreground: .white,
pulse: true
) {
let result = callManager.acceptIncomingCall()
#if DEBUG
if result != .started {
print("[Call] Accept button failed: \(result)")
}
#endif
}
}
.padding(.horizontal, 48)
}
// MARK: - Active: Mute + Speaker + End
private var activeButtons: some View {
HStack(spacing: 36) {
callButton(
title: state.isMuted ? "Unmute" : "Mute",
icon: state.isMuted ? "mic.slash.fill" : "mic.fill",
background: state.isMuted ? .white : Color.white.opacity(0.12),
foreground: state.isMuted ? .black : .white
) {
callManager.toggleMute()
}
callButton(
title: state.isSpeakerOn ? "Earpiece" : "Speaker",
icon: state.isSpeakerOn ? "speaker.wave.2.fill" : "speaker.slash.fill",
background: state.isSpeakerOn ? .white : Color.white.opacity(0.12),
foreground: state.isSpeakerOn ? .black : .white
) {
callManager.toggleSpeaker()
}
callButton(
title: "End",
icon: "phone.down.fill",
background: RosettaColors.error,
foreground: .white
) {
callManager.endCall()
}
}
}
// MARK: - Button Component
@ViewBuilder
private func callButton(
title: String,
icon: String,
background: Color,
foreground: Color,
pulse: Bool = false,
action: @escaping () -> Void
) -> some View {
Button(action: action) {
VStack(spacing: 8) {
ZStack {
Circle()
.fill(background)
.frame(width: 56, height: 56)
Image(systemName: icon)
.font(.system(size: 22, weight: .semibold))
.foregroundStyle(foreground)
}
.modifier(PulseModifier(isActive: pulse))
Text(title)
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.white.opacity(0.92))
}
}
.buttonStyle(CallButtonStyle())
}
}
// MARK: - Press Animation Style
private struct CallButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.9 : 1.0)
.animation(.spring(response: 0.2, dampingFraction: 0.6), value: configuration.isPressed)
}
}
// MARK: - Accept Pulse Modifier
private struct PulseModifier: ViewModifier {
let isActive: Bool
@State private var isPulsing = false
func body(content: Content) -> some View {
content
.scaleEffect(isActive && isPulsing ? 1.06 : 1.0)
.animation(
isActive && isPulsing
? .easeInOut(duration: 1.2).repeatForever(autoreverses: true)
: .default,
value: isPulsing
)
.onAppear {
if isActive { isPulsing = true }
}
.onChange(of: isActive) { _, active in
isPulsing = active
}
}
}