189 lines
5.9 KiB
Swift
189 lines
5.9 KiB
Swift
import SwiftUI
|
|
|
|
struct ActiveCallOverlayView: View {
|
|
@ObservedObject var callManager: CallManager
|
|
|
|
private var state: CallUiState {
|
|
callManager.uiState
|
|
}
|
|
|
|
private var durationText: String {
|
|
let duration = max(state.durationSec, 0)
|
|
let minutes = duration / 60
|
|
let seconds = duration % 60
|
|
return String(format: "%02d:%02d", minutes, seconds)
|
|
}
|
|
|
|
private var peerInitials: String {
|
|
let name = state.peerTitle.isEmpty ? state.peerUsername : state.peerTitle
|
|
guard !name.isEmpty else { return "?" }
|
|
let parts = name.split(separator: " ")
|
|
if parts.count >= 2 {
|
|
return "\(parts[0].prefix(1))\(parts[1].prefix(1))".uppercased()
|
|
}
|
|
return String(name.prefix(2)).uppercased()
|
|
}
|
|
|
|
private var peerColorIndex: Int {
|
|
guard !state.peerPublicKey.isEmpty else { return 0 }
|
|
return abs(state.peerPublicKey.hashValue) % 7
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.black.opacity(0.7)
|
|
.ignoresSafeArea()
|
|
|
|
VStack(spacing: 20) {
|
|
avatarSection
|
|
|
|
Text(state.displayName)
|
|
.font(.system(size: 22, weight: .semibold))
|
|
.foregroundStyle(.white)
|
|
.multilineTextAlignment(.center)
|
|
.lineLimit(2)
|
|
|
|
if state.phase == .active {
|
|
Text(durationText)
|
|
.font(.system(size: 17, weight: .medium))
|
|
.foregroundStyle(Color.white.opacity(0.85))
|
|
} else {
|
|
Text(statusText(for: state.phase))
|
|
.font(.system(size: 17, weight: .medium))
|
|
.foregroundStyle(Color.white.opacity(0.85))
|
|
}
|
|
|
|
controls
|
|
}
|
|
.padding(28)
|
|
.frame(maxWidth: 360)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 26, style: .continuous)
|
|
.fill(Color.black.opacity(0.62))
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 26, style: .continuous)
|
|
.stroke(Color.white.opacity(0.15), lineWidth: 1)
|
|
)
|
|
.padding(.horizontal, 24)
|
|
}
|
|
.transition(.opacity.combined(with: .scale(scale: 0.95)))
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var avatarSection: some View {
|
|
ZStack {
|
|
if state.phase != .active {
|
|
PulsingRings()
|
|
}
|
|
|
|
AvatarView(
|
|
initials: peerInitials,
|
|
colorIndex: peerColorIndex,
|
|
size: 90
|
|
)
|
|
}
|
|
.frame(width: 130, height: 130)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var controls: some View {
|
|
if state.phase == .incoming {
|
|
HStack(spacing: 16) {
|
|
callActionButton(
|
|
title: "Decline",
|
|
icon: "phone.down.fill",
|
|
color: RosettaColors.error
|
|
) {
|
|
callManager.declineIncomingCall()
|
|
}
|
|
callActionButton(
|
|
title: "Accept",
|
|
icon: "phone.fill",
|
|
color: RosettaColors.success
|
|
) {
|
|
_ = callManager.acceptIncomingCall()
|
|
}
|
|
}
|
|
} else {
|
|
HStack(spacing: 16) {
|
|
callActionButton(
|
|
title: state.isMuted ? "Unmute" : "Mute",
|
|
icon: state.isMuted ? "mic.slash.fill" : "mic.fill",
|
|
color: Color.white.opacity(0.18)
|
|
) {
|
|
callManager.toggleMute()
|
|
}
|
|
callActionButton(
|
|
title: state.isSpeakerOn ? "Earpiece" : "Speaker",
|
|
icon: state.isSpeakerOn ? "speaker.slash.fill" : "speaker.wave.2.fill",
|
|
color: Color.white.opacity(0.18)
|
|
) {
|
|
callManager.toggleSpeaker()
|
|
}
|
|
callActionButton(
|
|
title: "End",
|
|
icon: "phone.down.fill",
|
|
color: RosettaColors.error
|
|
) {
|
|
callManager.endCall()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func callActionButton(
|
|
title: String,
|
|
icon: String,
|
|
color: Color,
|
|
action: @escaping () -> Void
|
|
) -> some View {
|
|
Button(action: action) {
|
|
VStack(spacing: 8) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 20, weight: .semibold))
|
|
.foregroundStyle(.white)
|
|
.frame(width: 52, height: 52)
|
|
.background(Circle().fill(color))
|
|
Text(title)
|
|
.font(.system(size: 13, weight: .medium))
|
|
.foregroundStyle(.white.opacity(0.92))
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
private func statusText(for phase: CallPhase) -> String {
|
|
switch phase {
|
|
case .incoming: return "Incoming call"
|
|
case .outgoing: return "Calling..."
|
|
case .keyExchange: return "Exchanging keys..."
|
|
case .webRtcExchange: return "Connecting..."
|
|
case .active: return "Active"
|
|
case .ended: return "Ended"
|
|
case .idle: return ""
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct PulsingRings: View {
|
|
@State private var animate = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
ForEach(0..<3, id: \.self) { index in
|
|
Circle()
|
|
.stroke(Color.white.opacity(0.08 - Double(index) * 0.02), lineWidth: 1.5)
|
|
.scaleEffect(animate ? 1.0 + CGFloat(index + 1) * 0.12 : 1.0)
|
|
.opacity(animate ? 0.0 : 0.6)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.easeInOut(duration: 3.0).repeatForever(autoreverses: false)) {
|
|
animate = true
|
|
}
|
|
}
|
|
}
|
|
}
|