Add recording player

This commit is contained in:
Benoit Martins 2025-12-01 11:28:36 +01:00
parent b462657a77
commit 221e3cbb4b
8 changed files with 421 additions and 47 deletions

View file

@ -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
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="180" cy="164" r="28" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="52" cy="196" r="28" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="208" y1="72" x2="80" y2="104" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="80 196 80 56 208 24 208 164" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>

After

Width:  |  Height:  |  Size: 664 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#4e6074" viewBox="0 0 256 256"><path d="M176,160a39.89,39.89,0,0,0-28.62,12.09l-46.1-29.63a39.8,39.8,0,0,0,0-28.92l46.1-29.63a40,40,0,1,0-8.66-13.45l-46.1,29.63a40,40,0,1,0,0,55.82l46.1,29.63A40,40,0,1,0,176,160Zm0-128a24,24,0,1,1-24,24A24,24,0,0,1,176,32ZM64,152a24,24,0,1,1,24-24A24,24,0,0,1,64,152Zm112,72a24,24,0,1,1,24-24A24,24,0,0,1,176,224Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M176,160a39.89,39.89,0,0,0-28.62,12.09l-46.1-29.63a39.8,39.8,0,0,0,0-28.92l46.1-29.63a40,40,0,1,0-8.66-13.45l-46.1,29.63a40,40,0,1,0,0,55.82l46.1,29.63A40,40,0,1,0,176,160Zm0-128a24,24,0,1,1-24,24A24,24,0,0,1,176,32ZM64,152a24,24,0,1,1,24-24A24,24,0,0,1,64,152Zm112,72a24,24,0,1,1,24-24A24,24,0,0,1,176,224Z"></path></svg>

Before

Width:  |  Height:  |  Size: 431 B

After

Width:  |  Height:  |  Size: 431 B

View file

@ -86,13 +86,3 @@ final class CustomHostingController<Content: View>: UIHostingController<Content>
}
}
}
public struct LazyView<Content: View>: View {
private let build: () -> Content
public init(_ build: @autoclosure @escaping () -> Content) {
self.build = build
}
public var body: Content {
build()
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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) {}
}

View file

@ -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<Content: View>: View {
let build: () -> Content
init(_ build: @escaping () -> Content) {
self.build = build
}
var body: some View { build() }
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
}
}
}
}

View file

@ -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 = "<group>"; };
D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListFragment.swift; sourceTree = "<group>"; };
D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicSearchSingleton.swift; sourceTree = "<group>"; };
D7D1F5252EDD91B10034EEB0 /* RecordingMediaPlayerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingMediaPlayerFragment.swift; sourceTree = "<group>"; };
D7D1F5272EDD939D0034EEB0 /* RecordingMediaPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingMediaPlayerViewModel.swift; sourceTree = "<group>"; };
D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Medium.ttf"; sourceTree = "<group>"; };
D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Regular.ttf"; sourceTree = "<group>"; };
D7D24D0F2AC1B4E800C6F35B /* NotoSans-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Light.ttf"; sourceTree = "<group>"; };
@ -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 */,