diff --git a/Linphone/Assets.xcassets/music-notes.imageset/Contents.json b/Linphone/Assets.xcassets/music-notes.imageset/Contents.json
new file mode 100644
index 000000000..6194ada41
--- /dev/null
+++ b/Linphone/Assets.xcassets/music-notes.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "music-notes.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Linphone/Assets.xcassets/music-notes.imageset/music-notes.svg b/Linphone/Assets.xcassets/music-notes.imageset/music-notes.svg
new file mode 100644
index 000000000..fbf942b05
--- /dev/null
+++ b/Linphone/Assets.xcassets/music-notes.imageset/music-notes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Linphone/Assets.xcassets/share-network.imageset/share-network.svg b/Linphone/Assets.xcassets/share-network.imageset/share-network.svg
index 02d8619a1..835f930ac 100644
--- a/Linphone/Assets.xcassets/share-network.imageset/share-network.svg
+++ b/Linphone/Assets.xcassets/share-network.imageset/share-network.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/Linphone/UI/Main/Fragments/CustomBottomSheet.swift b/Linphone/UI/Main/Fragments/CustomBottomSheet.swift
index 18683b01f..b7a30e450 100644
--- a/Linphone/UI/Main/Fragments/CustomBottomSheet.swift
+++ b/Linphone/UI/Main/Fragments/CustomBottomSheet.swift
@@ -86,13 +86,3 @@ final class CustomHostingController: UIHostingController
}
}
}
-
-public struct LazyView: View {
- private let build: () -> Content
- public init(_ build: @autoclosure @escaping () -> Content) {
- self.build = build
- }
- public var body: Content {
- build()
- }
-}
diff --git a/Linphone/UI/Main/Recordings/Fragments/RecordingMediaPlayerFragment.swift b/Linphone/UI/Main/Recordings/Fragments/RecordingMediaPlayerFragment.swift
new file mode 100644
index 000000000..6d7d90eca
--- /dev/null
+++ b/Linphone/UI/Main/Recordings/Fragments/RecordingMediaPlayerFragment.swift
@@ -0,0 +1,238 @@
+/*
+ * 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 .
+ */
+
+import SwiftUI
+
+struct RecordingMediaPlayerFragment: View {
+
+ @StateObject private var recordingMediaPlayerViewModel: RecordingMediaPlayerViewModel
+
+ @Environment(\.dismiss) var dismiss
+
+ @State private var showShareSheet: Bool = false
+ @State private var showPicker: Bool = false
+ @State private var value: Double = 40.0
+ @State private var timer: Timer?
+
+ init(recording: RecordingModel) {
+ _recordingMediaPlayerViewModel = StateObject(wrappedValue: RecordingMediaPlayerViewModel(recording: recording))
+ }
+
+ var body: some View {
+ ZStack {
+ GeometryReader { geometry in
+ 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 {
+ dismiss()
+ }
+ }
+
+ VStack {
+ Text(recordingMediaPlayerViewModel.recording.displayName)
+ .default_text_style_700(styleSize: 14)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .lineLimit(1)
+
+ Text(recordingMediaPlayerViewModel.recording.dateTime)
+ .default_text_style_300(styleSize: 12)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .lineLimit(1)
+ }
+ .padding(.top, 4)
+
+ Spacer()
+
+ Button {
+ showShareSheet = true
+ } label: {
+ Image("share-network")
+ .renderingMode(.template)
+ .resizable()
+ .frame(width: 28, height: 28)
+ .foregroundStyle(Color.grayMain2c500)
+ }
+ .padding(.all, 6)
+ .padding(.top, 4)
+
+
+ Button {
+ showPicker = true
+ } label: {
+ Image("download-simple")
+ .renderingMode(.template)
+ .resizable()
+ .frame(width: 28, height: 28)
+ .foregroundStyle(Color.grayMain2c500)
+ }
+ .padding(.all, 6)
+ .padding(.top, 4)
+ }
+ .frame(maxWidth: .infinity)
+ .frame(height: 50)
+ .padding(.horizontal)
+ .padding(.bottom, 4)
+ .background(.white)
+
+ VStack {
+ Spacer()
+
+ Image("music-notes")
+ .renderingMode(.template)
+ .resizable()
+ .frame(width: 80, height: 80)
+ .foregroundStyle(.white)
+
+ Spacer()
+
+ HStack(spacing: 0) {
+ Button {
+ if recordingMediaPlayerViewModel.isPlaying {
+ recordingMediaPlayerViewModel.pauseVoiceRecordPlayer()
+ } else {
+ recordingMediaPlayerViewModel.startVoiceRecordPlayer()
+ playProgress()
+ }
+ } label: {
+ Image(recordingMediaPlayerViewModel.isPlaying ? "pause-fill" : "play-fill")
+ .renderingMode(.template)
+ .resizable()
+ .frame(width: 25, height: 25)
+ .foregroundStyle(.white)
+ }
+ .frame(width: 50)
+
+ let radius = geometry.size.height * 0.5
+ ZStack(alignment: .leading) {
+ Rectangle()
+ .foregroundColor(Color.orangeMain100)
+ .frame(width: (geometry.size.width - 120), height: 5)
+ .clipShape(RoundedRectangle(cornerRadius: radius))
+
+ Rectangle()
+ .foregroundColor(Color.orangeMain500)
+ .frame(width: self.value * (geometry.size.width - 120) / 100, height: 5)
+ .animation(self.value > 0 ? .linear(duration: 0.1) : nil, value: self.value)
+ .clipShape(RoundedRectangle(cornerRadius: radius))
+ }
+ .clipShape(RoundedRectangle(cornerRadius: radius))
+
+ Text(recordingMediaPlayerViewModel.recording.formattedDuration)
+ .default_text_style_white_600(styleSize: 18)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .lineLimit(1)
+ .frame(width: 70)
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(.black)
+ }
+ }
+ .sheet(isPresented: $showShareSheet) {
+ if let url = URL(string: "file://" + recordingMediaPlayerViewModel.recording.filePath) {
+ ShareAnySheet(items: [url])
+ .edgesIgnoringSafeArea(.bottom)
+ }
+ }
+ .sheet(isPresented: $showPicker) {
+ if let url = URL(string: "file://" + recordingMediaPlayerViewModel.recording.filePath) {
+ DocumentSaver(fileURL: url)
+ .edgesIgnoringSafeArea(.bottom)
+ }
+ }
+ .onAppear {
+ playProgress()
+ }
+ .onDisappear {
+ recordingMediaPlayerViewModel.stopVoiceRecordPlayer()
+ }
+ }
+ .navigationTitle("")
+ .navigationBarHidden(true)
+ }
+
+ private func playProgress() {
+ timer?.invalidate()
+
+ var lastValue = -1.0
+
+ value = recordingMediaPlayerViewModel.getPositionVoiceRecordPlayer()
+
+ timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
+ let current = recordingMediaPlayerViewModel.getPositionVoiceRecordPlayer()
+
+ if !recordingMediaPlayerViewModel.isPlaying {
+ self.value = current
+ lastValue = current
+ return
+ }
+
+ if current > lastValue {
+ self.value = current
+ lastValue = current
+ return
+ }
+
+ recordingMediaPlayerViewModel.stopVoiceRecordPlayer()
+ self.timer?.invalidate()
+ self.value = 0
+ }
+ }
+}
+
+struct SaveToFilesView: View {
+ let fileURL: URL
+ @State private var showPicker = false
+
+ var body: some View {
+ Button("Enregistrer dans Fichiers") {
+ showPicker = true
+ }
+ .sheet(isPresented: $showPicker) {
+ DocumentSaver(fileURL: fileURL)
+ }
+ }
+}
+
+struct DocumentSaver: UIViewControllerRepresentable {
+ let fileURL: URL
+
+ func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
+ let picker = UIDocumentPickerViewController(
+ forExporting: [fileURL], asCopy: true
+ )
+ return picker
+ }
+
+ func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {}
+}
diff --git a/Linphone/UI/Main/Recordings/Fragments/RecordingsListFragment.swift b/Linphone/UI/Main/Recordings/Fragments/RecordingsListFragment.swift
index 54cb18f7b..7b4920531 100644
--- a/Linphone/UI/Main/Recordings/Fragments/RecordingsListFragment.swift
+++ b/Linphone/UI/Main/Recordings/Fragments/RecordingsListFragment.swift
@@ -72,48 +72,52 @@ struct RecordingsListFragment: View {
.frame(maxWidth: .infinity, alignment: .leading)
}
- HStack {
- VStack {
- HStack {
- Image("phone")
- .renderingMode(.template)
- .resizable()
- .frame(width: 25, height: 25)
- .foregroundStyle(Color.grayMain2c600)
+ NavigationLink(destination: LazyView {
+ RecordingMediaPlayerFragment(recording: recording)
+ }) {
+ 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)
+ }
- Text(recording.displayName)
- .default_text_style_700(styleSize: 14)
+ Spacer()
+
+ Text(recording.dateTime)
+ .default_text_style(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)
}
-
- 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)
}
- .frame(height: 60)
- .padding(20)
- .background(.white)
- .clipShape(RoundedRectangle(cornerRadius: 10))
- .shadow(color: .gray.opacity(0.4), radius: 4)
}
}
.padding(.all, 20)
@@ -140,3 +144,11 @@ struct RecordingsListFragment: View {
.default_text_style_500(styleSize: 22)
}
}
+
+struct LazyView: View {
+ let build: () -> Content
+ init(_ build: @escaping () -> Content) {
+ self.build = build
+ }
+ var body: some View { build() }
+}
diff --git a/Linphone/UI/Main/Recordings/ViewModel/RecordingMediaPlayerViewModel.swift b/Linphone/UI/Main/Recordings/ViewModel/RecordingMediaPlayerViewModel.swift
new file mode 100644
index 000000000..a2b886b9a
--- /dev/null
+++ b/Linphone/UI/Main/Recordings/ViewModel/RecordingMediaPlayerViewModel.swift
@@ -0,0 +1,104 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import Combine
+
+class RecordingMediaPlayerViewModel: ObservableObject {
+ private let TAG = "[RecordingMediaPlayerViewModel]"
+
+ private var coreContext = CoreContext.shared
+
+ @Published var recording: RecordingModel
+
+ @Published var isPlaying: Bool = false
+
+ var vrpManager: VoiceRecordPlayerManager?
+
+ init(recording: RecordingModel) {
+ self.recording = recording
+ if let url = URL(string: "file://" + recording.filePath) {
+ startVoiceRecordPlayer(voiceRecordPath: url)
+ }
+ }
+
+ func startVoiceRecordPlayer(voiceRecordPath: URL) {
+ coreContext.doOnCoreQueue { core in
+ if self.vrpManager == nil || self.vrpManager!.voiceRecordPath != voiceRecordPath {
+ self.vrpManager = VoiceRecordPlayerManager(core: core, voiceRecordPath: voiceRecordPath)
+ }
+
+ if self.vrpManager != nil {
+ self.vrpManager!.startVoiceRecordPlayer()
+ DispatchQueue.main.async {
+ self.isPlaying = true
+ }
+ }
+ }
+ }
+
+ func getPositionVoiceRecordPlayer() -> Double {
+ if self.vrpManager != nil {
+ return self.vrpManager!.positionVoiceRecordPlayer()
+ } else {
+ return 0
+ }
+ }
+
+ func isPlayingVoiceRecordPlayer(voiceRecordPath: URL) -> Bool {
+ if self.vrpManager != nil && self.vrpManager!.voiceRecordPath == voiceRecordPath {
+ return true
+ } else {
+ return false
+ }
+ }
+
+ func startVoiceRecordPlayer() {
+ coreContext.doOnCoreQueue { _ in
+ if self.vrpManager != nil {
+ self.vrpManager!.startVoiceRecordPlayer()
+ DispatchQueue.main.async {
+ self.isPlaying = true
+ }
+ }
+ }
+ }
+
+ func pauseVoiceRecordPlayer() {
+ coreContext.doOnCoreQueue { _ in
+ if self.vrpManager != nil {
+ self.vrpManager!.pauseVoiceRecordPlayer()
+ DispatchQueue.main.async {
+ self.isPlaying = false
+ }
+ }
+ }
+ }
+
+ func stopVoiceRecordPlayer() {
+ coreContext.doOnCoreQueue { _ in
+ if self.vrpManager != nil {
+ self.vrpManager!.stopVoiceRecordPlayer()
+ DispatchQueue.main.async {
+ self.isPlaying = false
+ }
+ }
+ }
+ }
+}
diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj
index 2c8e765f1..e42ebfa0b 100644
--- a/LinphoneApp.xcodeproj/project.pbxproj
+++ b/LinphoneApp.xcodeproj/project.pbxproj
@@ -178,6 +178,8 @@
D7CEE03B2B7A234200FD79B7 /* ConversationsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */; };
D7CEE03D2B7A23B200FD79B7 /* ConversationsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */; };
D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */; };
+ D7D1F5262EDD91B30034EEB0 /* RecordingMediaPlayerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1F5252EDD91B10034EEB0 /* RecordingMediaPlayerFragment.swift */; };
+ D7D1F5282EDD939E0034EEB0 /* RecordingMediaPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1F5272EDD939D0034EEB0 /* RecordingMediaPlayerViewModel.swift */; };
D7D24D132AC1B4E800C6F35B /* NotoSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */; };
D7D24D142AC1B4E800C6F35B /* NotoSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */; };
D7D24D152AC1B4E800C6F35B /* NotoSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0F2AC1B4E800C6F35B /* NotoSans-Light.ttf */; };
@@ -414,6 +416,8 @@
D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsFragment.swift; sourceTree = ""; };
D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListFragment.swift; sourceTree = ""; };
D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicSearchSingleton.swift; sourceTree = ""; };
+ D7D1F5252EDD91B10034EEB0 /* RecordingMediaPlayerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingMediaPlayerFragment.swift; sourceTree = ""; };
+ D7D1F5272EDD939D0034EEB0 /* RecordingMediaPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingMediaPlayerViewModel.swift; sourceTree = ""; };
D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Medium.ttf"; sourceTree = ""; };
D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Regular.ttf"; sourceTree = ""; };
D7D24D0F2AC1B4E800C6F35B /* NotoSans-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Light.ttf"; sourceTree = ""; };
@@ -915,6 +919,7 @@
D795F57B2EC5F8FF0022C17D /* Fragments */ = {
isa = PBXGroup;
children = (
+ D7D1F5252EDD91B10034EEB0 /* RecordingMediaPlayerFragment.swift */,
D795F57D2EC5F9480022C17D /* RecordingsListFragment.swift */,
);
path = Fragments;
@@ -923,6 +928,7 @@
D795F57C2EC5F9090022C17D /* ViewModel */ = {
isa = PBXGroup;
children = (
+ D7D1F5272EDD939D0034EEB0 /* RecordingMediaPlayerViewModel.swift */,
D795F57F2EC5F95B0022C17D /* RecordingsListViewModel.swift */,
);
path = ViewModel;
@@ -1344,6 +1350,7 @@
D7C2DA1D2CA44DE400A2441B /* EventModel.swift in Sources */,
D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */,
D70A26F22B7F5D95006CC8FC /* ConversationFragment.swift in Sources */,
+ D7D1F5282EDD939E0034EEB0 /* RecordingMediaPlayerViewModel.swift in Sources */,
D7DC096F2CFA1D7600A6D47C /* AccountProfileFragment.swift in Sources */,
D717A10E2CEB772300849D92 /* ShareSheetController.swift in Sources */,
66C491FD2B24D36500CEA16D /* AudioRouteUtils.swift in Sources */,
@@ -1367,6 +1374,7 @@
C6A5A9452C10B6270070FEA4 /* OIDAuthStateExtension.swift in Sources */,
D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */,
D79F1C162CD3D6AD00FF0A05 /* ConversationInfoFragment.swift in Sources */,
+ D7D1F5262EDD91B30034EEB0 /* RecordingMediaPlayerFragment.swift in Sources */,
D7C500422D2BE98100DD53EC /* AccountSettingsViewModel.swift in Sources */,
D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */,
C67586B02C09F247002E77BF /* URIHandler.swift in Sources */,