153 lines
4.4 KiB
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
|
|
}
|
|
}
|
|
}
|