mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-01-17 11:08:06 +00:00
Add recording list
This commit is contained in:
parent
bcee4439f5
commit
b462657a77
12 changed files with 513 additions and 23 deletions
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" fill="#4e6074" viewBox="0 0 256 256"><path d="M222.37,158.46l-47.11-21.11-.13-.06a16,16,0,0,0-15.17,1.4,8.12,8.12,0,0,0-.75.56L134.87,160c-15.42-7.49-31.34-23.29-38.83-38.51l20.78-24.71c.2-.25.39-.5.57-.77a16,16,0,0,0,1.32-15.06l0-.12L97.54,33.64a16,16,0,0,0-16.62-9.52A56.26,56.26,0,0,0,32,80c0,79.4,64.6,144,144,144a56.26,56.26,0,0,0,55.88-48.92A16,16,0,0,0,222.37,158.46ZM176,208A128.14,128.14,0,0,1,48,80,40.2,40.2,0,0,1,82.87,40a.61.61,0,0,0,0,.12l21,47L83.2,111.86a6.13,6.13,0,0,0-.57.77,16,16,0,0,0-1,15.7c9.06,18.53,27.73,37.06,46.46,46.11a16,16,0,0,0,15.75-1.14,8.44,8.44,0,0,0,.74-.56L168.89,152l47,21.05h0s.08,0,.11,0A40.21,40.21,0,0,1,176,208Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M222.37,158.46l-47.11-21.11-.13-.06a16,16,0,0,0-15.17,1.4,8.12,8.12,0,0,0-.75.56L134.87,160c-15.42-7.49-31.34-23.29-38.83-38.51l20.78-24.71c.2-.25.39-.5.57-.77a16,16,0,0,0,1.32-15.06l0-.12L97.54,33.64a16,16,0,0,0-16.62-9.52A56.26,56.26,0,0,0,32,80c0,79.4,64.6,144,144,144a56.26,56.26,0,0,0,55.88-48.92A16,16,0,0,0,222.37,158.46ZM176,208A128.14,128.14,0,0,1,48,80,40.2,40.2,0,0,1,82.87,40a.61.61,0,0,0,0,.12l21,47L83.2,111.86a6.13,6.13,0,0,0-.57.77,16,16,0,0,0-1,15.7c9.06,18.53,27.73,37.06,46.46,46.11a16,16,0,0,0,15.75-1.14,8.44,8.44,0,0,0,.74-.56L168.89,152l47,21.05h0s.08,0,.11,0A40.21,40.21,0,0,1,176,208Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 735 B After Width: | Height: | Size: 733 B |
|
|
@ -200,6 +200,26 @@ class CoreContext: ObservableObject {
|
|||
self.forceRemotePushToMatchVoipPushSettings(account: acc)
|
||||
}
|
||||
|
||||
let container = FileUtil.sharedContainerUrl()
|
||||
let recordingsDir = container.appendingPathComponent("Library/Recordings")
|
||||
|
||||
let fm = FileManager.default
|
||||
|
||||
if !fm.fileExists(atPath: recordingsDir.path) {
|
||||
do {
|
||||
try fm.createDirectory(
|
||||
at: recordingsDir,
|
||||
withIntermediateDirectories: true,
|
||||
attributes: nil
|
||||
)
|
||||
print("Recordings directory created.")
|
||||
} catch {
|
||||
print("Error creating directory: \(error)")
|
||||
}
|
||||
} else {
|
||||
print("Recordings directory already exists.")
|
||||
}
|
||||
|
||||
self.mCoreDelegate = CoreDelegateStub(onGlobalStateChanged: { (core: Core, state: GlobalState, _: String) in
|
||||
if state == GlobalState.On {
|
||||
#if DEBUG
|
||||
|
|
|
|||
|
|
@ -195,17 +195,13 @@ class TelecomManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func makeRecordFilePath() -> String {
|
||||
var filePath = "recording_"
|
||||
let now = Date()
|
||||
let dateFormat = DateFormatter()
|
||||
dateFormat.dateFormat = "E-d-MMM-yyyy-HH-mm-ss"
|
||||
let date = dateFormat.string(from: now)
|
||||
filePath = filePath.appending("\(date).mkv")
|
||||
private func makeRecordFilePath(address: String) -> String {
|
||||
var filePath = "call_recording_sip_" + address.dropFirst(4) + "_on_"
|
||||
|
||||
let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)
|
||||
let writablePath = paths[0]
|
||||
return writablePath.appending("/\(filePath)")
|
||||
filePath = filePath.appending("\(Int(Date().timeIntervalSince1970)).mkv")
|
||||
|
||||
let writablePath = FileUtil.sharedContainerUrl().appendingPathComponent("Library/Recordings/\(filePath)")
|
||||
return writablePath.path
|
||||
}
|
||||
|
||||
func doCall(core: Core, addr: Address, isSas: Bool, isVideo: Bool, isConference: Bool = false) throws {
|
||||
|
|
@ -237,7 +233,7 @@ class TelecomManager: ObservableObject {
|
|||
// Log.directLog(BCTBX_LOG_DEBUG, text: "record file path: \(writablePath)")
|
||||
// lcallParams.recordFile = writablePath
|
||||
|
||||
lcallParams.recordFile = makeRecordFilePath()
|
||||
lcallParams.recordFile = makeRecordFilePath(address: addr.asStringUriOnly())
|
||||
|
||||
if isSas {
|
||||
lcallParams.mediaEncryption = .ZRTP
|
||||
|
|
@ -292,7 +288,7 @@ class TelecomManager: ObservableObject {
|
|||
func acceptCall(core: Core, call: Call, hasVideo: Bool) {
|
||||
do {
|
||||
let callParams = try core.createCallParams(call: call)
|
||||
callParams.recordFile = makeRecordFilePath()
|
||||
callParams.recordFile = makeRecordFilePath(address: call.remoteAddress?.asStringUriOnly() ?? "")
|
||||
callParams.videoEnabled = hasVideo
|
||||
/*if (ConfigManager.instance().lpConfigBoolForKey(key: "edge_opt_preference")) {
|
||||
let low_bandwidth = (AppManager.network() == .network_2g)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ struct ContentView: View {
|
|||
@State var isShowConversationFragment = false
|
||||
@State var isShowAccountProfileFragment = false
|
||||
@State var isShowSettingsFragment = false
|
||||
@State var isShowRecordingsListFragment = false
|
||||
@State var isShowHelpFragment = false
|
||||
|
||||
@State var fullscreenVideo = false
|
||||
|
|
@ -1041,6 +1042,7 @@ struct ContentView: View {
|
|||
isShowLoginFragment: $isShowLoginFragment,
|
||||
isShowAccountProfileFragment: $isShowAccountProfileFragment,
|
||||
isShowSettingsFragment: $isShowSettingsFragment,
|
||||
isShowRecordingsListFragment: $isShowRecordingsListFragment,
|
||||
isShowHelpFragment: $isShowHelpFragment
|
||||
)
|
||||
.environmentObject(accountProfileViewModel)
|
||||
|
|
@ -1285,6 +1287,14 @@ struct ContentView: View {
|
|||
.transition(.move(edge: .trailing))
|
||||
}
|
||||
|
||||
if isShowRecordingsListFragment {
|
||||
RecordingsListFragment(
|
||||
isShowRecordingsListFragment: $isShowRecordingsListFragment
|
||||
)
|
||||
.zIndex(3)
|
||||
.transition(.move(edge: .trailing))
|
||||
}
|
||||
|
||||
if isShowHelpFragment {
|
||||
HelpFragment(
|
||||
isShowHelpFragment: $isShowHelpFragment
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ struct SideMenu: View {
|
|||
@Binding var isShowLoginFragment: Bool
|
||||
@Binding var isShowAccountProfileFragment: Bool
|
||||
@Binding var isShowSettingsFragment: Bool
|
||||
@Binding var isShowRecordingsListFragment: Bool
|
||||
@Binding var isShowHelpFragment: Bool
|
||||
@State private var showHelp = false
|
||||
|
||||
|
|
@ -137,12 +138,15 @@ struct SideMenu: View {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
SideMenuEntry(
|
||||
iconName: "record-fill",
|
||||
title: "recordings_title"
|
||||
)
|
||||
*/
|
||||
).onTapGesture {
|
||||
self.menuClose()
|
||||
withAnimation {
|
||||
isShowRecordingsListFragment = true
|
||||
}
|
||||
}
|
||||
|
||||
SideMenuEntry(
|
||||
iconName: "question",
|
||||
|
|
@ -152,7 +156,6 @@ struct SideMenu: View {
|
|||
withAnimation {
|
||||
isShowHelpFragment = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.padding(.bottom, safeAreaInsets.bottom + 13)
|
||||
|
|
@ -176,15 +179,15 @@ struct SideMenu: View {
|
|||
|
||||
#Preview {
|
||||
GeometryReader { geometry in
|
||||
@State var triggerNavigateToLogin: Bool = false
|
||||
SideMenu(
|
||||
width: geometry.size.width / 5 * 4,
|
||||
isOpen: .constant(true),
|
||||
menuClose: {},
|
||||
safeAreaInsets: geometry.safeAreaInsets,
|
||||
isShowLoginFragment: $triggerNavigateToLogin,
|
||||
isShowLoginFragment: .constant(false),
|
||||
isShowAccountProfileFragment: .constant(false),
|
||||
isShowSettingsFragment: .constant(false),
|
||||
isShowRecordingsListFragment: .constant(false),
|
||||
isShowHelpFragment: .constant(false)
|
||||
)
|
||||
.ignoresSafeArea(.all)
|
||||
|
|
|
|||
|
|
@ -25,10 +25,6 @@ struct HelpFragment: View {
|
|||
|
||||
@Binding var isShowHelpFragment: Bool
|
||||
|
||||
@State var advancedSettingsIsOpen: Bool = false
|
||||
|
||||
@FocusState var isVoicemailUriFocused: Bool
|
||||
|
||||
var showAssistant: Bool {
|
||||
(CoreContext.shared.coreIsStarted && CoreContext.shared.accounts.isEmpty)
|
||||
|| SharedMainViewModel.shared.displayProfileMode
|
||||
|
|
|
|||
|
|
@ -152,8 +152,10 @@ struct HistoryRow: View {
|
|||
|
||||
if !historyModel.isConf {
|
||||
Image("phone")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.foregroundStyle(Color.grayMain2c600)
|
||||
.padding(.all, 10)
|
||||
.padding(.trailing, 5)
|
||||
.highPriorityGesture(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2023 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/>.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RecordingsListFragment: View {
|
||||
|
||||
@StateObject private var recordingsListViewModel = RecordingsListViewModel()
|
||||
|
||||
@Binding var isShowRecordingsListFragment: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
VStack(spacing: 1) {
|
||||
Rectangle()
|
||||
.foregroundColor(Color.orangeMain500)
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.frame(height: 0)
|
||||
|
||||
HStack {
|
||||
Image("caret-left")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
.padding(.top, 4)
|
||||
.padding(.leading, -10)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
isShowRecordingsListFragment = false
|
||||
}
|
||||
}
|
||||
|
||||
Text("recordings_title")
|
||||
.default_text_style_orange_800(styleSize: 16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 4)
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 4)
|
||||
.background(.white)
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 20) {
|
||||
ForEach(Array(recordingsListViewModel.recordings.enumerated()), id: \.offset) { index, recording in
|
||||
if index == 0 || recording.month != recordingsListViewModel.recordings[index-1].month {
|
||||
createMonthLine(model: recording)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
HStack {
|
||||
VStack {
|
||||
HStack {
|
||||
Image("phone")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.foregroundStyle(Color.grayMain2c600)
|
||||
|
||||
Text(recording.displayName)
|
||||
.default_text_style_700(styleSize: 14)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(recording.dateTime)
|
||||
.default_text_style(styleSize: 14)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
VStack {
|
||||
Image("play-fill")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.frame(width: 30, height: 30)
|
||||
.padding(.leading, -6)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(recording.formattedDuration)
|
||||
.default_text_style(styleSize: 14)
|
||||
.frame(alignment: .center)
|
||||
}
|
||||
.padding(.trailing, 6)
|
||||
}
|
||||
.frame(height: 60)
|
||||
.padding(20)
|
||||
.background(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.shadow(color: .gray.opacity(0.4), radius: 4)
|
||||
}
|
||||
}
|
||||
.padding(.all, 20)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.background(Color.gray100)
|
||||
}
|
||||
.background(Color.gray100)
|
||||
}
|
||||
.navigationTitle("")
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.navigationTitle("")
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func createMonthLine(model: RecordingModel) -> some View {
|
||||
Text(model.month)
|
||||
.fontWeight(.bold)
|
||||
.padding(5)
|
||||
.default_text_style_500(styleSize: 22)
|
||||
}
|
||||
}
|
||||
180
Linphone/UI/Main/Recordings/Models/RecordingModel.swift
Normal file
180
Linphone/UI/Main/Recordings/Models/RecordingModel.swift
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2023 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/>.
|
||||
*/
|
||||
|
||||
import linphonesw
|
||||
import SwiftUI
|
||||
|
||||
class RecordingModel: ObservableObject {
|
||||
static let TAG = "[Recording Model]"
|
||||
|
||||
var filePath: String = ""
|
||||
var fileName: String = ""
|
||||
var sipUri: String = ""
|
||||
var displayName: String = ""
|
||||
var timestamp: Int64 = 0
|
||||
var month: String = ""
|
||||
var dateTime: String = ""
|
||||
var formattedDuration: String = ""
|
||||
var duration: Int = 0
|
||||
|
||||
init(filePath: String, fileName: String, isLegacy: Bool = false) {
|
||||
self.filePath = filePath
|
||||
self.fileName = fileName
|
||||
|
||||
var sipUriTmp: String = ""
|
||||
var displayNameTmp: String = ""
|
||||
var timestampTmp: Int64 = 0
|
||||
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
if isLegacy {
|
||||
let parts = fileName.split(separator: "_")
|
||||
let username = String(parts.first ?? "")
|
||||
let sipAddress = core.interpretUrl(url: username, applyInternationalPrefix: false)
|
||||
sipUriTmp = sipAddress?.asStringUriOnly() ?? username
|
||||
|
||||
if let address = sipAddress {
|
||||
ContactsManager.shared.getFriendWithAddressInCoreQueue(address: address) { friendResult in
|
||||
if let addressFriend = friendResult {
|
||||
displayNameTmp = addressFriend.name!
|
||||
} else {
|
||||
if address.displayName != nil {
|
||||
displayNameTmp = address.displayName!
|
||||
} else if address.username != nil {
|
||||
displayNameTmp = address.username!
|
||||
} else {
|
||||
displayNameTmp = String(address.asStringUriOnly().dropFirst(4))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
displayNameTmp = sipUriTmp
|
||||
}
|
||||
|
||||
if parts.count > 1 {
|
||||
let parsedDate = String(parts[1])
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "dd-MM-yyyy-HH-mm-ss"
|
||||
if let date = formatter.date(from: parsedDate) {
|
||||
timestampTmp = Int64(date.timeIntervalSince1970 * 1000)
|
||||
} else {
|
||||
Log.error("\(RecordingModel.TAG) Failed to parse legacy timestamp \(parsedDate)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let headerLength = LinphoneUtils.RECORDING_FILE_NAME_HEADER.count
|
||||
let withoutHeader = String(fileName.dropFirst(headerLength))
|
||||
guard let sepRange = withoutHeader.range(of: LinphoneUtils.RECORDING_FILE_NAME_URI_TIMESTAMP_SEPARATOR) else {
|
||||
fatalError("\(RecordingModel.TAG) Invalid file name format \(withoutHeader)")
|
||||
}
|
||||
|
||||
sipUriTmp = String(withoutHeader[..<sepRange.lowerBound])
|
||||
let sipAddress = try? Factory.Instance.createAddress(addr: "sip:" + sipUriTmp)
|
||||
|
||||
if let address = sipAddress {
|
||||
ContactsManager.shared.getFriendWithAddressInCoreQueue(address: address) { friendResult in
|
||||
if let addressFriend = friendResult {
|
||||
displayNameTmp = addressFriend.name!
|
||||
} else {
|
||||
if address.displayName != nil {
|
||||
displayNameTmp = address.displayName!
|
||||
} else if address.username != nil {
|
||||
displayNameTmp = address.username!
|
||||
} else {
|
||||
displayNameTmp = String(address.asStringUriOnly().dropFirst(4))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
displayNameTmp = sipUriTmp
|
||||
}
|
||||
|
||||
let start = sepRange.upperBound
|
||||
if let dotRange = withoutHeader.range(of: ".", options: .backwards) {
|
||||
let parsedTimestamp = String(withoutHeader[start..<dotRange.lowerBound])
|
||||
Log.info("\(RecordingModel.TAG) Extract SIP URI \(sipUriTmp) and timestamp \(parsedTimestamp) from file \(fileName)")
|
||||
timestampTmp = Int64(parsedTimestamp) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
self.sipUri = sipUriTmp
|
||||
self.displayName = displayNameTmp
|
||||
self.timestamp = timestampTmp
|
||||
|
||||
self.month = self.formattedMonthYear(timestamp: timestampTmp)
|
||||
self.dateTime = self.formattedDateTime(timestamp: timestampTmp)
|
||||
|
||||
do {
|
||||
let audioPlayer = try core.createLocalPlayer(soundCardName: nil, videoDisplayName: nil, windowId: nil)
|
||||
try? audioPlayer.open(filename: filePath)
|
||||
self.duration = audioPlayer.duration
|
||||
print("durationduration \(audioPlayer.duration) \((audioPlayer.duration/1000).convertDurationToString())")
|
||||
self.formattedDuration = (audioPlayer.duration/1000).convertDurationToString()
|
||||
} catch {
|
||||
self.duration = 0
|
||||
self.formattedDuration = "??:??"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func delete() async {
|
||||
Log.info("\(RecordingModel.TAG) Deleting call recording \(filePath)")
|
||||
//await FileUtils.deleteFile(path: filePath)
|
||||
}
|
||||
|
||||
func formattedDateTime(timestamp: Int64) -> String {
|
||||
let locale = Locale.current
|
||||
let date = Date(timeIntervalSince1970: TimeInterval(timestamp))
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = locale
|
||||
|
||||
if Calendar.current.isDate(date, equalTo: .now, toGranularity: .year) {
|
||||
dateFormatter.dateFormat = locale.identifier == "fr_FR"
|
||||
? "EEEE d MMMM"
|
||||
: "EEEE, MMMM d"
|
||||
} else {
|
||||
dateFormatter.dateFormat = locale.identifier == "fr_FR"
|
||||
? "EEEE d MMMM yyyy"
|
||||
: "EEEE, MMMM d, yyyy"
|
||||
}
|
||||
|
||||
let timeFormatter = DateFormatter()
|
||||
timeFormatter.locale = locale
|
||||
timeFormatter.dateFormat = locale.identifier == "fr_FR"
|
||||
? "HH:mm"
|
||||
: "h:mm a"
|
||||
|
||||
return "\(dateFormatter.string(from: date).capitalized) - \(timeFormatter.string(from: date))"
|
||||
}
|
||||
|
||||
func formattedMonthYear(timestamp: Int64) -> String {
|
||||
let locale = Locale.current
|
||||
let date = Date(timeIntervalSince1970: TimeInterval(timestamp))
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = locale
|
||||
if Calendar.current.isDate(date, equalTo: .now, toGranularity: .year) {
|
||||
formatter.dateFormat = "MMMM"
|
||||
} else {
|
||||
formatter.dateFormat = "MMMM yyyy"
|
||||
}
|
||||
|
||||
return formatter.string(from: date).capitalized
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2023 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/>.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class RecordingsListViewModel: ObservableObject {
|
||||
private let TAG = "[RecordingsListViewModel]"
|
||||
|
||||
@Published var recordings: [RecordingModel] = []
|
||||
@Published var searchBarVisible: Bool = false
|
||||
@Published var searchFilter: String = ""
|
||||
@Published var fetchInProgress: Bool = true
|
||||
|
||||
@Published var focusSearchBarEvent: Bool? = nil
|
||||
|
||||
private let legacyRecordRegex = try! NSRegularExpression(pattern: ".*/(.*)_(\\d{2}-\\d{2}-\\d{4}-\\d{2}-\\d{2}-\\d{2})\\..*")
|
||||
|
||||
init() {
|
||||
fetchInProgress = true
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
self.computeList(filter: "")
|
||||
}
|
||||
}
|
||||
|
||||
func openSearchBar() {
|
||||
searchBarVisible = true
|
||||
focusSearchBarEvent = true
|
||||
}
|
||||
|
||||
func closeSearchBar() {
|
||||
clearFilter()
|
||||
searchBarVisible = false
|
||||
focusSearchBarEvent = false
|
||||
}
|
||||
|
||||
func clearFilter() {
|
||||
if searchFilter.isEmpty {
|
||||
searchBarVisible = false
|
||||
focusSearchBarEvent = false
|
||||
} else {
|
||||
searchFilter = ""
|
||||
}
|
||||
}
|
||||
|
||||
func applyFilter(_ filter: String) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
self.computeList(filter: filter)
|
||||
}
|
||||
}
|
||||
|
||||
private func computeList(filter: String) {
|
||||
var list: [RecordingModel] = []
|
||||
|
||||
let dir1 = FileUtil.sharedContainerUrl().appendingPathComponent("Library/Recordings")
|
||||
if let files = try? FileManager.default.contentsOfDirectory(at: dir1, includingPropertiesForKeys: nil) {
|
||||
for file in files {
|
||||
let path = file.path
|
||||
let name = file.lastPathComponent
|
||||
|
||||
let model = RecordingModel(filePath: path, fileName: name)
|
||||
|
||||
if filter.isEmpty || model.sipUri.contains(filter) {
|
||||
list.append(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.sort { $0.timestamp > $1.timestamp }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.recordings = list
|
||||
self.fetchInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,11 @@ import Foundation
|
|||
import linphonesw
|
||||
|
||||
class LinphoneUtils: NSObject {
|
||||
static let RECORDING_FILE_NAME_HEADER = "call_recording_sip_"
|
||||
static let RECORDING_FILE_NAME_URI_TIMESTAMP_SEPARATOR = "_on_"
|
||||
static let RECORDING_MKV_FILE_EXTENSION = ".mkv"
|
||||
static let RECORDING_SMFF_FILE_EXTENSION = ".smff"
|
||||
|
||||
public class func isChatRoomAGroup(chatRoom: ChatRoom) -> Bool {
|
||||
let oneToOne = chatRoom.hasCapability(mask: ChatRoom.Capabilities.OneToOne.rawValue)
|
||||
let conference = chatRoom.hasCapability(mask: ChatRoom.Capabilities.Conference.rawValue)
|
||||
|
|
|
|||
|
|
@ -147,6 +147,9 @@
|
|||
D78E062C2BEA69BC00CE3783 /* CallStatisticsSheetBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E062B2BEA69BC00CE3783 /* CallStatisticsSheetBottomSheet.swift */; };
|
||||
D78E062E2BEA69F400CE3783 /* AudioRouteBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E062D2BEA69F400CE3783 /* AudioRouteBottomSheet.swift */; };
|
||||
D78E06302BEA6A4A00CE3783 /* ChangeLayoutBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E062F2BEA6A4A00CE3783 /* ChangeLayoutBottomSheet.swift */; };
|
||||
D795F57E2EC5F9500022C17D /* RecordingsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D795F57D2EC5F9480022C17D /* RecordingsListFragment.swift */; };
|
||||
D795F5802EC5F9660022C17D /* RecordingsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D795F57F2EC5F95B0022C17D /* RecordingsListViewModel.swift */; };
|
||||
D795F5832EC6133C0022C17D /* RecordingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D795F5822EC6133A0022C17D /* RecordingModel.swift */; };
|
||||
D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79622332B1DFE600037EACD /* DialerBottomSheet.swift */; };
|
||||
D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */; };
|
||||
D79F1C162CD3D6AD00FF0A05 /* ConversationInfoFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79F1C152CD3D6AD00FF0A05 /* ConversationInfoFragment.swift */; };
|
||||
|
|
@ -379,6 +382,9 @@
|
|||
D78E062B2BEA69BC00CE3783 /* CallStatisticsSheetBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallStatisticsSheetBottomSheet.swift; sourceTree = "<group>"; };
|
||||
D78E062D2BEA69F400CE3783 /* AudioRouteBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRouteBottomSheet.swift; sourceTree = "<group>"; };
|
||||
D78E062F2BEA6A4A00CE3783 /* ChangeLayoutBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLayoutBottomSheet.swift; sourceTree = "<group>"; };
|
||||
D795F57D2EC5F9480022C17D /* RecordingsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingsListFragment.swift; sourceTree = "<group>"; };
|
||||
D795F57F2EC5F95B0022C17D /* RecordingsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingsListViewModel.swift; sourceTree = "<group>"; };
|
||||
D795F5822EC6133A0022C17D /* RecordingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingModel.swift; sourceTree = "<group>"; };
|
||||
D79622332B1DFE600037EACD /* DialerBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialerBottomSheet.swift; sourceTree = "<group>"; };
|
||||
D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = "<group>"; };
|
||||
D79F1C152CD3D6AD00FF0A05 /* ConversationInfoFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationInfoFragment.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -695,6 +701,7 @@
|
|||
D7A03FBE2ACC2E010081A588 /* History */,
|
||||
66E56BC52BA45E49006CE56F /* Meetings */,
|
||||
66D382032CEB7DB80063E1C5 /* Models */,
|
||||
D795F57A2EC5F89B0022C17D /* Recordings */,
|
||||
D7DC096A2CFA192200A6D47C /* Settings */,
|
||||
D7A2EDD42AC180FE005D90FC /* Viewmodel */,
|
||||
D719ABB82ABC67BF00B41C10 /* ContentView.swift */,
|
||||
|
|
@ -895,6 +902,40 @@
|
|||
path = ViewModel;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D795F57A2EC5F89B0022C17D /* Recordings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D795F57B2EC5F8FF0022C17D /* Fragments */,
|
||||
D795F5812EC613220022C17D /* Models */,
|
||||
D795F57C2EC5F9090022C17D /* ViewModel */,
|
||||
);
|
||||
path = Recordings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D795F57B2EC5F8FF0022C17D /* Fragments */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D795F57D2EC5F9480022C17D /* RecordingsListFragment.swift */,
|
||||
);
|
||||
path = Fragments;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D795F57C2EC5F9090022C17D /* ViewModel */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D795F57F2EC5F95B0022C17D /* RecordingsListViewModel.swift */,
|
||||
);
|
||||
path = ViewModel;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D795F5812EC613220022C17D /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D795F5822EC6133A0022C17D /* RecordingModel.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D7A03FBB2ACC2D850081A588 /* Contacts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1280,6 +1321,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */,
|
||||
D795F57E2EC5F9500022C17D /* RecordingsListFragment.swift in Sources */,
|
||||
D7ADF6002AFE356400212231 /* Avatar.swift in Sources */,
|
||||
D7CEE03B2B7A234200FD79B7 /* ConversationsFragment.swift in Sources */,
|
||||
D71707202AC5989C0037746F /* TextExtension.swift in Sources */,
|
||||
|
|
@ -1380,8 +1422,10 @@
|
|||
D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */,
|
||||
D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */,
|
||||
6613A0B42BAEBE3F008923A4 /* MeetingViewModel.swift in Sources */,
|
||||
D795F5832EC6133C0022C17D /* RecordingModel.swift in Sources */,
|
||||
D7173EBE2B7A5C0A00BCC481 /* LinphoneUtils.swift in Sources */,
|
||||
66C492012B24DB6900CEA16D /* Log.swift in Sources */,
|
||||
D795F5802EC5F9660022C17D /* RecordingsListViewModel.swift in Sources */,
|
||||
D756C8182D352C5F00A58F2F /* CorePreferences.swift in Sources */,
|
||||
C6A5A9432C10B5ED0070FEA4 /* DecodableExtension.swift in Sources */,
|
||||
D714035B2BE11E00004BD8CA /* CallMediaEncryptionModel.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue