Add a bottom sheet in RecordingsListFragment and display an empty state when the list is empty

This commit is contained in:
Benoit Martins 2025-12-04 16:33:15 +01:00
parent 36fa752ccf
commit fe8432f128
11 changed files with 351 additions and 103 deletions

View file

@ -1,5 +1,5 @@
import Foundation
public let APP_GIT_BRANCH = "master"
public let APP_GIT_COMMIT = "5492a3e3a"
public let APP_GIT_COMMIT = "36fa752cc"
public let APP_GIT_TAG = "6.1.0-alpha"

View file

@ -406,6 +406,8 @@
"menu_resend_chat_message" = "Re-send";
"menu_see_existing_contact" = "See contact";
"menu_show_imdn" = "Delivery status";
"menu_export_selected_item" = "Download";
"menu_share_selected_item" = "Share";
"message_copied_to_clipboard_toast" = "Message copied into clipboard";
"message_delivery_info_error_title" = "Error";
"message_delivery_info_read_title" = "Read";
@ -435,6 +437,10 @@
"picker_categories" = "Categories";
"qr_code_validated" = "QR code validated";
"recordings_title" = "Recordings";
"recordings_list_empty" = "No recording for the moment…";
"recordings_list_empty" = "No recording for the moment…";
"recordings_list_empty" = "No recording for the moment…";
"recordings_list_empty" = "No recording for the moment…";
"selected_participants_count" = "%@ selected participants";
"settings_advanced_accept_early_media_title" = "Accept early media";
"settings_advanced_allow_outgoing_early_media_title" = "Allow outgoing early media";

View file

@ -406,6 +406,8 @@
"menu_resend_chat_message" = "Ré-envoyer";
"menu_see_existing_contact" = "Voir le contact";
"menu_show_imdn" = "Info de réception";
"menu_export_selected_item" = "Télécharger";
"menu_share_selected_item" = "Partager";
"message_copied_to_clipboard_toast" = "Message copié dans le presse-papier";
"message_delivery_info_error_title" = "En erreur";
"message_delivery_info_read_title" = "Lu";
@ -435,6 +437,7 @@
"picker_categories" = "Catégories";
"qr_code_validated" = "QR code validé";
"recordings_title" = "Enregistrements";
"recordings_list_empty" = "Aucun appel enregistré…";
"selected_participants_count" = "%@ participants selectionnés";
"settings_advanced_accept_early_media_title" = "Accepter l'early media";
"settings_advanced_allow_outgoing_early_media_title" = "Autoriser l'early media pour les appels sortants";

View file

@ -48,7 +48,7 @@ struct MeetingWaitingRoomFragment: View {
.sheet(isPresented: $audioRouteSheet, onDismiss: {
audioRouteSheet = false
}, content: {
innerBottomSheet().presentationDetents([.fraction(0.3)])
innerBottomSheet().presentationDetents([.fraction(0.4)])
})
.onAppear {
meetingWaitingRoomViewModel.enableAVAudioSession()

View file

@ -41,7 +41,7 @@ struct ContactsFragment: View {
showingSheet: $showingSheet,
showShareSheet: $showShareSheet
)
.presentationDetents(contactsListViewModel.selectedFriend?.isReadOnly == true ? [.fraction(0.1)] : [.fraction(0.3)])
.presentationDetents(contactsListViewModel.selectedFriend?.isReadOnly == true ? [.fraction(0.1)] : [.fraction(0.4)])
}
.sheet(isPresented: $showShareSheet) {
ShareSheet(friendToShare: contactsListViewModel.selectedFriendToShare!)

View file

@ -39,7 +39,7 @@ struct HistoryFragment: View {
isShowEditContactFragment: $isShowEditContactFragment,
isShowEditContactFragmentAddress: $isShowEditContactFragmentAddress
)
.presentationDetents([.fraction(0.3)])
.presentationDetents([.fraction(0.4)])
}
} else {
HistoryListFragment(showingSheet: $showingSheet, text: $text)

View file

@ -0,0 +1,176 @@
/*
* 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
import linphonesw
struct RecordingsListBottomSheet: View {
@Environment(\.dismiss) var dismiss
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@State private var orientation = UIDevice.current.orientation
@EnvironmentObject var recordingsListViewModel: RecordingsListViewModel
@Binding var showingSheet: Bool
@Binding var showShareSheet: Bool
@Binding var showPicker: Bool
var body: some View {
VStack(alignment: .leading) {
if idiom != .pad && (orientation == .landscapeLeft
|| orientation == .landscapeRight
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
Spacer()
HStack {
Spacer()
Button("dialog_close") {
if #available(iOS 16.0, *) {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
}
}
.padding(.trailing)
}
Spacer()
if let selectedRecording = recordingsListViewModel.selectedRecording {
Button {
showShareSheet = true
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
} label: {
HStack {
Image("share-network")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("menu_share_selected_item")
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxWidth: .infinity)
Button {
showPicker = true
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
} label: {
HStack {
Image("download-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("menu_export_selected_item")
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxWidth: .infinity)
Button {
recordingsListViewModel.recordings.removeAll { $0.filePath == selectedRecording.filePath }
selectedRecording.delete()
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
} label: {
HStack {
Image("trash")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("menu_delete_selected_item")
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
}
}
.background(Color.gray100)
.frame(maxWidth: .infinity)
.onRotate { newOrientation in
orientation = newOrientation
}
}
}
#Preview {
ConversationsListBottomSheet(showingSheet: .constant(true))
}

View file

@ -23,11 +23,54 @@ struct RecordingsListFragment: View {
@StateObject private var recordingsListViewModel = RecordingsListViewModel()
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@Binding var isShowRecordingsListFragment: Bool
@State var showingSheet: Bool = false
@State private var showShareSheet: Bool = false
@State private var showPicker: Bool = false
var body: some View {
NavigationView {
ZStack {
if #available(iOS 16.4, *), idiom != .pad {
innerView()
.sheet(isPresented: $showingSheet) {
RecordingsListBottomSheet(showingSheet: $showingSheet, showShareSheet: $showShareSheet, showPicker: $showPicker)
.environmentObject(recordingsListViewModel)
.presentationDetents([.fraction(0.4)])
}
} else {
innerView()
.halfSheet(showSheet: $showingSheet) {
RecordingsListBottomSheet(showingSheet: $showingSheet, showShareSheet: $showShareSheet, showPicker: $showPicker)
.environmentObject(recordingsListViewModel)
} onDismiss: {}
}
}
.sheet(isPresented: $showShareSheet) {
if let selectedRecording = recordingsListViewModel.selectedRecording, let url = URL(string: "file://" + selectedRecording.filePath) {
ShareAnySheet(items: [url])
.edgesIgnoringSafeArea(.bottom)
}
}
.sheet(isPresented: $showPicker) {
if let selectedRecording = recordingsListViewModel.selectedRecording, let url = URL(string: "file://" + selectedRecording.filePath) {
DocumentSaver(fileURL: url)
.edgesIgnoringSafeArea(.bottom)
}
}
.navigationTitle("")
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationTitle("")
.navigationBarHidden(true)
}
@ViewBuilder
func innerView() -> some View {
VStack(spacing: 1) {
Rectangle()
.foregroundColor(Color.orangeMain500)
@ -117,6 +160,11 @@ struct RecordingsListFragment: View {
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 10))
.shadow(color: .gray.opacity(0.4), radius: 4)
.onLongPressGesture(minimumDuration: 0.3) {
touchFeedback()
recordingsListViewModel.selectedRecording = recording
showingSheet = true
}
}
}
}
@ -124,17 +172,26 @@ struct RecordingsListFragment: View {
}
.frame(maxWidth: .infinity)
}
.overlay(
VStack {
if recordingsListViewModel.recordings.count == 0 {
Spacer()
Image("illus-belledonne")
.resizable()
.scaledToFit()
.clipped()
.padding()
Text("recordings_list_empty")
.default_text_style_800(styleSize: 16)
Spacer()
Spacer()
}
}
)
.background(Color.gray100)
}
.background(Color.gray100)
}
.navigationTitle("")
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationTitle("")
.navigationBarHidden(true)
}
@ViewBuilder
func createMonthLine(model: RecordingModel) -> some View {

View file

@ -132,9 +132,9 @@ class RecordingModel: ObservableObject {
}
}
func delete() async {
func delete() {
Log.info("\(RecordingModel.TAG) Deleting call recording \(filePath)")
//await FileUtils.deleteFile(path: filePath)
FileUtil.delete(path: filePath)
}
func formattedDateTime(timestamp: Int64) -> String {

View file

@ -30,6 +30,8 @@ class RecordingsListViewModel: ObservableObject {
@Published var focusSearchBarEvent: Bool? = nil
var selectedRecording: RecordingModel?
private let legacyRecordRegex = try! NSRegularExpression(pattern: ".*/(.*)_(\\d{2}-\\d{2}-\\d{4}-\\d{2}-\\d{2}-\\d{2})\\..*")
init() {

View file

@ -116,6 +116,7 @@
D734499B2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */; };
D737AEEF2DA011F2005C1280 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D737AEED2DA011F2005C1280 /* Localizable.strings */; };
D738ACEE2E857BF10039F7D1 /* KeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D738ACED2E857BEF0039F7D1 /* KeyboardResponder.swift */; };
D73DD7D02EE184B200654313 /* RecordingsListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73DD7CF2EE184A500654313 /* RecordingsListBottomSheet.swift */; };
D7458F392E0BDCF4000C957A /* linphoneExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */; };
D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */; };
@ -355,6 +356,7 @@
D737AEEE2DA011F2005C1280 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable/en.lproj/Localizable.strings; sourceTree = "<group>"; };
D737AEF02DA01203005C1280 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable/fr.lproj/Localizable.strings; sourceTree = "<group>"; };
D738ACED2E857BEF0039F7D1 /* KeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardResponder.swift; sourceTree = "<group>"; };
D73DD7CF2EE184A500654313 /* RecordingsListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingsListBottomSheet.swift; sourceTree = "<group>"; };
D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = linphoneExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = "<group>"; };
D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = "<group>"; };
@ -922,6 +924,7 @@
D795F57B2EC5F8FF0022C17D /* Fragments */ = {
isa = PBXGroup;
children = (
D73DD7CF2EE184A500654313 /* RecordingsListBottomSheet.swift */,
D7D1F5252EDD91B10034EEB0 /* RecordingMediaPlayerFragment.swift */,
D795F57D2EC5F9480022C17D /* RecordingsListFragment.swift */,
);
@ -1434,6 +1437,7 @@
66162A202BDFC2F900DCE913 /* AddParticipantsViewModel.swift in Sources */,
D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */,
D78E06302BEA6A4A00CE3783 /* ChangeLayoutBottomSheet.swift in Sources */,
D73DD7D02EE184B200654313 /* RecordingsListBottomSheet.swift in Sources */,
66E56BC92BA4A6D7006CE56F /* MeetingsListViewModel.swift in Sources */,
D726E4392B16440C0083C415 /* ContactAvatarModel.swift in Sources */,
C67586AE2C09F23C002E77BF /* URLExtension.swift in Sources */,