mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-01-17 19:18:06 +00:00
1030 lines
30 KiB
Swift
1030 lines
30 KiB
Swift
/*
|
|
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
|
*
|
|
* This file is part of linphone-iphone
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
// swiftlint:disable type_body_length
|
|
// swiftlint:disable line_length
|
|
import SwiftUI
|
|
import CallKit
|
|
import AVFAudio
|
|
import linphonesw
|
|
|
|
struct CallView: View {
|
|
|
|
@ObservedObject private var coreContext = CoreContext.shared
|
|
@ObservedObject private var telecomManager = TelecomManager.shared
|
|
@ObservedObject private var contactsManager = ContactsManager.shared
|
|
|
|
@ObservedObject var callViewModel: CallViewModel
|
|
|
|
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
|
@State private var orientation = UIDevice.current.orientation
|
|
|
|
let pub = NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification)
|
|
|
|
@State var startDate = Date.now
|
|
@State var audioRouteSheet: Bool = false
|
|
@State var hideButtonsSheet: Bool = false
|
|
@State var options: Int = 1
|
|
|
|
@State var imageAudioRoute: String = ""
|
|
|
|
@State var angleDegree = 0.0
|
|
@State var fullscreenVideo = false
|
|
|
|
var body: some View {
|
|
GeometryReader { geo in
|
|
if #available(iOS 16.0, *), idiom != .pad {
|
|
innerView(geometry: geo)
|
|
.sheet(isPresented: $audioRouteSheet, onDismiss: {
|
|
audioRouteSheet = false
|
|
hideButtonsSheet = false
|
|
}) {
|
|
innerBottomSheet()
|
|
.presentationDetents([.fraction(0.3)])
|
|
}
|
|
} else {
|
|
innerView(geometry: geo)
|
|
.halfSheet(showSheet: $audioRouteSheet) {
|
|
innerBottomSheet()
|
|
} onDismiss: {
|
|
audioRouteSheet = false
|
|
hideButtonsSheet = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
func innerBottomSheet() -> some View {
|
|
VStack(spacing: 0) {
|
|
Button(action: {
|
|
options = 1
|
|
|
|
do {
|
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
|
if callViewModel.isHeadPhoneAvailable() {
|
|
try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Receiver") }).first)
|
|
} else {
|
|
try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.first)
|
|
}
|
|
} catch _ {
|
|
|
|
}
|
|
}, label: {
|
|
HStack {
|
|
Image(options == 1 ? "radio-button-fill" : "radio-button")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(.white)
|
|
.frame(width: 25, height: 25, alignment: .leading)
|
|
|
|
Text(!callViewModel.isHeadPhoneAvailable() ? "Earpiece" : "Headphones")
|
|
.default_text_style_white(styleSize: 15)
|
|
|
|
Spacer()
|
|
|
|
Image(!callViewModel.isHeadPhoneAvailable() ? "ear" : "headset")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(.white)
|
|
.frame(width: 25, height: 25, alignment: .leading)
|
|
}
|
|
})
|
|
.frame(maxHeight: .infinity)
|
|
|
|
Button(action: {
|
|
options = 2
|
|
|
|
do {
|
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
|
|
} catch _ {
|
|
|
|
}
|
|
}, label: {
|
|
HStack {
|
|
Image(options == 2 ? "radio-button-fill" : "radio-button")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(.white)
|
|
.frame(width: 25, height: 25, alignment: .leading)
|
|
|
|
Text("Speaker")
|
|
.default_text_style_white(styleSize: 15)
|
|
|
|
Spacer()
|
|
|
|
Image("speaker-high")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(.white)
|
|
.frame(width: 25, height: 25, alignment: .leading)
|
|
}
|
|
})
|
|
.frame(maxHeight: .infinity)
|
|
|
|
Button(action: {
|
|
options = 3
|
|
|
|
do {
|
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
|
try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Bluetooth") }).first)
|
|
} catch _ {
|
|
|
|
}
|
|
}, label: {
|
|
HStack {
|
|
Image(options == 3 ? "radio-button-fill" : "radio-button")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(.white)
|
|
.frame(width: 25, height: 25, alignment: .leading)
|
|
|
|
Text("Bluetooth")
|
|
.default_text_style_white(styleSize: 15)
|
|
|
|
Spacer()
|
|
|
|
Image("bluetooth")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(.white)
|
|
.frame(width: 25, height: 25, alignment: .leading)
|
|
}
|
|
})
|
|
.frame(maxHeight: .infinity)
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.background(Color.gray600)
|
|
.frame(maxHeight: .infinity)
|
|
}
|
|
|
|
@ViewBuilder
|
|
// swiftlint:disable:next cyclomatic_complexity
|
|
func innerView(geometry: GeometryProxy) -> some View {
|
|
VStack {
|
|
if !fullscreenVideo {
|
|
if #available(iOS 16.0, *) {
|
|
Rectangle()
|
|
.foregroundColor(Color.orangeMain500)
|
|
.edgesIgnoringSafeArea(.top)
|
|
.frame(height: 0)
|
|
} else if idiom != .pad && !(orientation == .landscapeLeft || orientation == .landscapeRight
|
|
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
|
|
Rectangle()
|
|
.foregroundColor(Color.orangeMain500)
|
|
.edgesIgnoringSafeArea(.top)
|
|
.frame(height: 1)
|
|
}
|
|
|
|
HStack {
|
|
if callViewModel.direction == .Outgoing {
|
|
Image("outgoing-call")
|
|
.resizable()
|
|
.frame(width: 15, height: 15)
|
|
.padding(.horizontal)
|
|
|
|
Text("Outgoing call")
|
|
.foregroundStyle(.white)
|
|
} else {
|
|
Image("incoming-call")
|
|
.resizable()
|
|
.frame(width: 15, height: 15)
|
|
.padding(.horizontal)
|
|
|
|
Text("Incoming call")
|
|
.foregroundStyle(.white)
|
|
}
|
|
|
|
if !telecomManager.outgoingCallStarted && telecomManager.callInProgress {
|
|
Text("|")
|
|
.foregroundStyle(.white)
|
|
|
|
ZStack {
|
|
Text(callViewModel.timeElapsed.convertDurationToString())
|
|
.onReceive(callViewModel.timer) { firedDate in
|
|
callViewModel.timeElapsed = Int(firedDate.timeIntervalSince(startDate))
|
|
}
|
|
.foregroundStyle(.white)
|
|
.if(callViewModel.isPaused || telecomManager.isPausedByRemote) { view in
|
|
view.hidden()
|
|
}
|
|
|
|
if callViewModel.isPaused {
|
|
Text("Paused")
|
|
.foregroundStyle(.white)
|
|
} else if telecomManager.isPausedByRemote {
|
|
Text("Paused by remote")
|
|
.foregroundStyle(.white)
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if telecomManager.remoteVideo {
|
|
Button {
|
|
callViewModel.switchCamera()
|
|
} label: {
|
|
Image("camera-rotate")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(.white)
|
|
.frame(width: 30, height: 30)
|
|
.padding(.horizontal)
|
|
}
|
|
}
|
|
}
|
|
.frame(height: 40)
|
|
.zIndex(1)
|
|
}
|
|
|
|
ZStack {
|
|
VStack {
|
|
Spacer()
|
|
|
|
if callViewModel.remoteAddress != nil {
|
|
let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.remoteAddress!)
|
|
|
|
let contactAvatarModel = addressFriend != nil
|
|
? ContactsManager.shared.avatarListModel.first(where: {
|
|
($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy)
|
|
&& $0.friend!.name == addressFriend!.name
|
|
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
|
|
})
|
|
: ContactAvatarModel(friend: nil, withPresence: false)
|
|
|
|
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
|
|
if contactAvatarModel != nil {
|
|
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 100, hidePresence: true)
|
|
}
|
|
} else {
|
|
if callViewModel.remoteAddress!.displayName != nil {
|
|
Image(uiImage: contactsManager.textToImage(
|
|
firstName: callViewModel.remoteAddress!.displayName!,
|
|
lastName: callViewModel.remoteAddress!.displayName!.components(separatedBy: " ").count > 1
|
|
? callViewModel.remoteAddress!.displayName!.components(separatedBy: " ")[1]
|
|
: ""))
|
|
.resizable()
|
|
.frame(width: 100, height: 100)
|
|
.clipShape(Circle())
|
|
|
|
} else {
|
|
Image(uiImage: contactsManager.textToImage(
|
|
firstName: callViewModel.remoteAddress!.username ?? "Username Error",
|
|
lastName: callViewModel.remoteAddress!.username!.components(separatedBy: " ").count > 1
|
|
? callViewModel.remoteAddress!.username!.components(separatedBy: " ")[1]
|
|
: ""))
|
|
.resizable()
|
|
.frame(width: 100, height: 100)
|
|
.clipShape(Circle())
|
|
}
|
|
|
|
}
|
|
} else {
|
|
Image("profil-picture-default")
|
|
.resizable()
|
|
.frame(width: 100, height: 100)
|
|
.clipShape(Circle())
|
|
}
|
|
|
|
Text(callViewModel.displayName)
|
|
.padding(.top)
|
|
.foregroundStyle(.white)
|
|
|
|
Text(callViewModel.remoteAddressString)
|
|
.foregroundStyle(.white)
|
|
|
|
Spacer()
|
|
}
|
|
|
|
LinphoneVideoViewHolder { view in
|
|
coreContext.doOnCoreQueue { core in
|
|
core.nativeVideoWindow = view
|
|
}
|
|
}
|
|
.frame(
|
|
width:
|
|
angleDegree == 0
|
|
? 120 * ((geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) / 160)
|
|
: 120 * ((geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) / 120),
|
|
height:
|
|
angleDegree == 0
|
|
? 160 * ((geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) / 160)
|
|
: 160 * ((geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) / 120)
|
|
)
|
|
.scaledToFill()
|
|
.clipped()
|
|
.onTapGesture {
|
|
if telecomManager.remoteVideo {
|
|
fullscreenVideo.toggle()
|
|
}
|
|
}
|
|
|
|
if telecomManager.remoteVideo {
|
|
HStack {
|
|
Spacer()
|
|
VStack {
|
|
Spacer()
|
|
LinphoneVideoViewHolder { view in
|
|
coreContext.doOnCoreQueue { core in
|
|
core.nativePreviewWindow = view
|
|
}
|
|
}
|
|
.frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2)
|
|
.cornerRadius(20)
|
|
.padding(10)
|
|
.padding(.trailing, abs(angleDegree/2))
|
|
}
|
|
}
|
|
.frame(
|
|
maxWidth: fullscreenVideo ? geometry.size.width : geometry.size.width - 8,
|
|
maxHeight: fullscreenVideo ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - 140
|
|
)
|
|
}
|
|
|
|
if callViewModel.isRecording {
|
|
HStack {
|
|
VStack {
|
|
Image("record-fill")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(Color.redDanger500)
|
|
.frame(width: 32, height: 32)
|
|
.padding(10)
|
|
.if(fullscreenVideo) { view in
|
|
view.padding(.top, 30)
|
|
}
|
|
Spacer()
|
|
}
|
|
Spacer()
|
|
}
|
|
.frame(
|
|
maxWidth: fullscreenVideo ? geometry.size.width : geometry.size.width - 8,
|
|
maxHeight: fullscreenVideo ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - 140
|
|
)
|
|
}
|
|
|
|
if telecomManager.outgoingCallStarted {
|
|
VStack {
|
|
ActivityIndicator()
|
|
.frame(width: 20, height: 20)
|
|
.padding(.top, 100)
|
|
|
|
Text(callViewModel.counterToMinutes())
|
|
.onAppear {
|
|
callViewModel.timeElapsed = 0
|
|
startDate = Date.now
|
|
}
|
|
.onReceive(callViewModel.timer) { firedDate in
|
|
callViewModel.timeElapsed = Int(firedDate.timeIntervalSince(startDate))
|
|
|
|
}
|
|
.onDisappear {
|
|
callViewModel.timeElapsed = 0
|
|
startDate = Date.now
|
|
}
|
|
.padding(.top)
|
|
.foregroundStyle(.white)
|
|
|
|
Spacer()
|
|
}
|
|
.frame(
|
|
maxWidth: fullscreenVideo ? geometry.size.width : geometry.size.width - 8,
|
|
maxHeight: fullscreenVideo ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (0.1 * geometry.size.height) - 60
|
|
)
|
|
.background(.clear)
|
|
}
|
|
}
|
|
.frame(
|
|
maxWidth: fullscreenVideo ? geometry.size.width : geometry.size.width - 8,
|
|
maxHeight: fullscreenVideo ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (0.1 * geometry.size.height) - 60
|
|
)
|
|
.background(Color.gray600)
|
|
.cornerRadius(20)
|
|
.padding(.horizontal, fullscreenVideo ? 0 : 4)
|
|
.onRotate { newOrientation in
|
|
let oldOrientation = orientation
|
|
orientation = newOrientation
|
|
if orientation == .portrait || orientation == .portraitUpsideDown {
|
|
angleDegree = 0
|
|
} else {
|
|
if orientation == .landscapeLeft {
|
|
angleDegree = -90
|
|
} else if orientation == .landscapeRight {
|
|
angleDegree = 90
|
|
}
|
|
}
|
|
|
|
if (oldOrientation != orientation && oldOrientation != .faceUp) || (oldOrientation == .faceUp && (orientation == .landscapeLeft || orientation == .landscapeRight)) {
|
|
telecomManager.callStarted = false
|
|
|
|
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
|
|
telecomManager.callStarted = true
|
|
}
|
|
}
|
|
|
|
callViewModel.orientationUpdate(orientation: orientation)
|
|
}
|
|
.onAppear {
|
|
if orientation == .portrait && orientation == .portraitUpsideDown {
|
|
angleDegree = 0
|
|
} else {
|
|
if orientation == .landscapeLeft {
|
|
angleDegree = -90
|
|
} else if orientation == .landscapeRight {
|
|
angleDegree = 90
|
|
}
|
|
}
|
|
|
|
telecomManager.callStarted = false
|
|
|
|
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
|
|
telecomManager.callStarted = true
|
|
}
|
|
|
|
callViewModel.orientationUpdate(orientation: orientation)
|
|
}
|
|
|
|
if !fullscreenVideo {
|
|
if telecomManager.callStarted {
|
|
let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
|
|
let bottomInset = scene?.windows.first?.safeAreaInsets
|
|
|
|
BottomSheetView(
|
|
content: bottomSheetContent(geo: geometry),
|
|
minHeight: (0.1 * geometry.size.height) + (bottomInset != nil ? bottomInset!.bottom : 0),
|
|
maxHeight: (0.45 * geometry.size.height) + (bottomInset != nil ? bottomInset!.bottom : 0),
|
|
currentHeight: (0.1 * geometry.size.height) + (bottomInset != nil ? bottomInset!.bottom : 0)
|
|
)
|
|
} else {
|
|
#if targetEnvironment(simulator)
|
|
HStack(spacing: 12) {
|
|
HStack {
|
|
Spacer()
|
|
|
|
Button {
|
|
callViewModel.terminateCall()
|
|
} label: {
|
|
Image("phone-disconnect")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(.white)
|
|
.frame(width: 32, height: 32)
|
|
|
|
}
|
|
.frame(width: 90, height: 60)
|
|
.background(Color.redDanger500)
|
|
.cornerRadius(40)
|
|
|
|
Button {
|
|
callViewModel.acceptCall()
|
|
} label: {
|
|
Image("phone")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(.white)
|
|
.frame(width: 32, height: 32)
|
|
|
|
}
|
|
.frame(width: 90, height: 60)
|
|
.background(Color.greenSuccess500)
|
|
.cornerRadius(40)
|
|
|
|
Spacer()
|
|
}
|
|
.frame(height: 60)
|
|
}
|
|
.padding(.horizontal, 25)
|
|
.padding(.top, 20)
|
|
#else
|
|
HStack(spacing: 12) {
|
|
HStack {
|
|
}
|
|
.frame(height: 60)
|
|
}
|
|
.padding(.horizontal, 25)
|
|
.padding(.top, 20)
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(Color.gray900)
|
|
.if(fullscreenVideo) { view in
|
|
view.ignoresSafeArea(.all)
|
|
}
|
|
}
|
|
|
|
func bottomSheetContent(geo: GeometryProxy) -> some View {
|
|
GeometryReader { _ in
|
|
VStack(spacing: 0) {
|
|
Rectangle()
|
|
.fill(Color.gray500)
|
|
.frame(width: 100, height: 5)
|
|
.cornerRadius(10)
|
|
.padding(.top, 5)
|
|
HStack(spacing: 12) {
|
|
Button {
|
|
callViewModel.terminateCall()
|
|
} label: {
|
|
Image("phone-disconnect")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(.white)
|
|
.frame(width: 32, height: 32)
|
|
|
|
}
|
|
.frame(width: 90, height: 60)
|
|
.background(Color.redDanger500)
|
|
.cornerRadius(40)
|
|
|
|
Spacer()
|
|
|
|
Button {
|
|
callViewModel.toggleVideo()
|
|
} label: {
|
|
Image(callViewModel.cameraDisplayed ? "video-camera" : "video-camera-slash")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white)
|
|
.frame(width: 32, height: 32)
|
|
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500)
|
|
.cornerRadius(40)
|
|
.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote)
|
|
|
|
Button {
|
|
callViewModel.toggleMuteMicrophone()
|
|
} label: {
|
|
Image(callViewModel.micMutted ? "microphone-slash" : "microphone")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(callViewModel.micMutted ? .black : .white)
|
|
.frame(width: 32, height: 32)
|
|
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(callViewModel.micMutted ? .white : Color.gray500)
|
|
.cornerRadius(40)
|
|
|
|
Button {
|
|
if AVAudioSession.sharedInstance().availableInputs != nil
|
|
&& !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty {
|
|
|
|
hideButtonsSheet = true
|
|
|
|
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
|
|
audioRouteSheet = true
|
|
}
|
|
} else {
|
|
do {
|
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty ? .speaker : .none)
|
|
} catch _ {
|
|
|
|
}
|
|
}
|
|
|
|
} label: {
|
|
Image(imageAudioRoute)
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(.white)
|
|
.frame(width: 32, height: 32)
|
|
.onAppear(perform: getAudioRouteImage)
|
|
.onReceive(pub) { _ in
|
|
self.getAudioRouteImage()
|
|
}
|
|
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(Color.gray500)
|
|
.cornerRadius(40)
|
|
}
|
|
.frame(height: geo.size.height * 0.15)
|
|
.padding(.horizontal, 20)
|
|
.padding(.top, (orientation != .landscapeLeft && orientation != .landscapeRight) ? (geo.safeAreaInsets.bottom != 0 ? -15 : -30) : -10)
|
|
|
|
if orientation != .landscapeLeft && orientation != .landscapeRight {
|
|
HStack(spacing: 0) {
|
|
VStack {
|
|
Button {
|
|
} label: {
|
|
Image("phone-transfer")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(Color.gray500)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(Color.gray600)
|
|
.cornerRadius(40)
|
|
.disabled(true)
|
|
|
|
Text("Transfer")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
|
|
|
|
VStack {
|
|
Button {
|
|
} label: {
|
|
Image("phone-plus")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(Color.gray500)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(Color.gray600)
|
|
.cornerRadius(40)
|
|
.disabled(true)
|
|
|
|
Text("New call")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
|
|
|
|
VStack {
|
|
Button {
|
|
} label: {
|
|
Image("phone-list")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(Color.gray500)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(Color.gray600)
|
|
.cornerRadius(40)
|
|
.disabled(true)
|
|
|
|
Text("Call list")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
|
|
|
|
VStack {
|
|
Button {
|
|
} label: {
|
|
Image("dialer")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(Color.gray500)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(Color.gray600)
|
|
.cornerRadius(40)
|
|
.disabled(true)
|
|
|
|
Text("Dialer")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
|
|
|
|
|
|
}
|
|
.frame(height: geo.size.height * 0.15)
|
|
|
|
HStack(spacing: 0) {
|
|
VStack {
|
|
Button {
|
|
} label: {
|
|
Image("chat-teardrop-text")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
//.foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white)
|
|
.foregroundStyle(Color.gray500)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
//.background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500)
|
|
.background(Color.gray600)
|
|
.cornerRadius(40)
|
|
//.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote)
|
|
.disabled(true)
|
|
|
|
Text("Messages")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
|
|
|
|
VStack {
|
|
Button {
|
|
callViewModel.togglePause()
|
|
} label: {
|
|
Image(callViewModel.isPaused ? "play" : "pause")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(telecomManager.isPausedByRemote ? Color.gray500 : .white)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(telecomManager.isPausedByRemote ? Color.gray600 : (callViewModel.isPaused ? Color.greenSuccess500 : Color.gray500))
|
|
.cornerRadius(40)
|
|
.disabled(telecomManager.isPausedByRemote)
|
|
|
|
Text("Pause")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
|
|
|
|
VStack {
|
|
Button {
|
|
callViewModel.toggleRecording()
|
|
} label: {
|
|
Image("record-fill")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : (callViewModel.isRecording ? Color.redDanger500 : Color.gray500))
|
|
.cornerRadius(40)
|
|
.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote)
|
|
|
|
Text("Record")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
|
|
|
|
VStack {
|
|
Button {
|
|
} label: {
|
|
Image("video-camera")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(.white)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(Color.gray500)
|
|
.cornerRadius(40)
|
|
|
|
Text("Disposition")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
|
|
.hidden()
|
|
}
|
|
.frame(height: geo.size.height * 0.15)
|
|
} else {
|
|
HStack {
|
|
VStack {
|
|
Button {
|
|
} label: {
|
|
Image("phone-transfer")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(Color.gray500)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(Color.gray600)
|
|
.cornerRadius(40)
|
|
.disabled(true)
|
|
|
|
Text("Transfer")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125)
|
|
|
|
VStack {
|
|
Button {
|
|
} label: {
|
|
Image("phone-plus")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(Color.gray500)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(Color.gray600)
|
|
.cornerRadius(40)
|
|
.disabled(true)
|
|
|
|
Text("New call")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125)
|
|
|
|
VStack {
|
|
Button {
|
|
} label: {
|
|
Image("phone-list")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(Color.gray500)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(Color.gray600)
|
|
.cornerRadius(40)
|
|
.disabled(true)
|
|
|
|
Text("Call list")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125)
|
|
|
|
VStack {
|
|
Button {
|
|
} label: {
|
|
Image("dialer")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(Color.gray500)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(Color.gray600)
|
|
.cornerRadius(40)
|
|
.disabled(true)
|
|
|
|
Text("Dialer")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125)
|
|
|
|
|
|
|
|
VStack {
|
|
Button {
|
|
} label: {
|
|
Image("chat-teardrop-text")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
//.foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white)
|
|
.foregroundStyle(Color.gray500)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
//.background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500)
|
|
.background(Color.gray600)
|
|
.cornerRadius(40)
|
|
//.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote)
|
|
.disabled(true)
|
|
|
|
Text("Messages")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125)
|
|
|
|
VStack {
|
|
Button {
|
|
callViewModel.togglePause()
|
|
} label: {
|
|
Image(callViewModel.isPaused ? "play" : "pause")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle(telecomManager.isPausedByRemote ? Color.gray500 : .white)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background(telecomManager.isPausedByRemote ? Color.gray600 : (callViewModel.isPaused ? Color.greenSuccess500 : Color.gray500))
|
|
.cornerRadius(40)
|
|
.disabled(telecomManager.isPausedByRemote)
|
|
|
|
Text("Pause")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125)
|
|
|
|
VStack {
|
|
Button {
|
|
callViewModel.toggleRecording()
|
|
} label: {
|
|
Image("record-fill")
|
|
.renderingMode(.template)
|
|
.resizable()
|
|
.foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white)
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.frame(width: 60, height: 60)
|
|
.background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : (callViewModel.isRecording ? Color.redDanger500 : Color.gray500))
|
|
.cornerRadius(40)
|
|
.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote)
|
|
|
|
Text("Record")
|
|
.foregroundStyle(.white)
|
|
.default_text_style(styleSize: 15)
|
|
}
|
|
.frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125)
|
|
}
|
|
.frame(height: geo.size.height * 0.15)
|
|
.padding(.horizontal, 20)
|
|
.padding(.top, 30)
|
|
}
|
|
Spacer()
|
|
}
|
|
.background(Color.gray900)
|
|
.frame(maxHeight: .infinity, alignment: .top)
|
|
}
|
|
}
|
|
|
|
func getAudioRouteImage() {
|
|
imageAudioRoute = AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty
|
|
? (
|
|
AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty
|
|
? (
|
|
callViewModel.isHeadPhoneAvailable()
|
|
? "headset"
|
|
: "speaker-slash"
|
|
)
|
|
: "bluetooth"
|
|
)
|
|
: "speaker-high"
|
|
}
|
|
}
|
|
|
|
struct BottomSheetView<Content: View>: View {
|
|
let content: Content
|
|
|
|
@State var minHeight: CGFloat
|
|
@State var maxHeight: CGFloat
|
|
|
|
@State var currentHeight: CGFloat
|
|
|
|
var body: some View {
|
|
GeometryReader { geometry in
|
|
VStack(spacing: 0.0) {
|
|
content
|
|
}
|
|
.onAppear {
|
|
self.currentHeight = minHeight
|
|
}
|
|
.frame(
|
|
width: geometry.size.width,
|
|
height: maxHeight,
|
|
alignment: .top
|
|
)
|
|
.clipShape(
|
|
Path(
|
|
UIBezierPath(
|
|
roundedRect: CGRect(x: 0.0, y: 0.0, width: geometry.size.width, height: maxHeight),
|
|
byRoundingCorners: [.topLeft, .topRight],
|
|
cornerRadii: CGSize(width: 16.0, height: 16.0)
|
|
)
|
|
.cgPath
|
|
)
|
|
)
|
|
.frame(
|
|
height: geometry.size.height,
|
|
alignment: .bottom
|
|
)
|
|
.highPriorityGesture(
|
|
DragGesture()
|
|
.onChanged { value in
|
|
currentHeight -= value.translation.height
|
|
currentHeight = min(max(currentHeight, minHeight), maxHeight)
|
|
}
|
|
.onEnded { _ in
|
|
withAnimation {
|
|
currentHeight = (currentHeight - minHeight <= maxHeight - currentHeight) ? minHeight : maxHeight
|
|
}
|
|
}
|
|
)
|
|
.offset(y: maxHeight - currentHeight)
|
|
}
|
|
.edgesIgnoringSafeArea(.bottom)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
CallView(callViewModel: CallViewModel())
|
|
}
|
|
// swiftlint:enable type_body_length
|
|
// swiftlint:enable line_length
|