mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-01-17 11:08:06 +00:00
Add edit contact view
This commit is contained in:
parent
ac7f4da260
commit
abd5461f54
19 changed files with 1643 additions and 702 deletions
|
|
@ -47,6 +47,9 @@
|
|||
D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A2EDD52AC18115005D90FC /* SharedMainViewModel.swift */; };
|
||||
D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */; };
|
||||
D7C365082AEFAB7F00FE6142 /* ContactListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */; };
|
||||
D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365092AF001C300FE6142 /* EditContactFragment.swift */; };
|
||||
D7C3650C2AF0084000FE6142 /* EditContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */; };
|
||||
D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */; };
|
||||
D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.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 */; };
|
||||
|
|
@ -110,6 +113,9 @@
|
|||
D7A2EDDA2AC19EEC005D90FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerFragment.swift; sourceTree = "<group>"; };
|
||||
D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListBottomSheet.swift; sourceTree = "<group>"; };
|
||||
D7C365092AF001C300FE6142 /* EditContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactFragment.swift; sourceTree = "<group>"; };
|
||||
D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactViewModel.swift; sourceTree = "<group>"; };
|
||||
D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = "<group>"; };
|
||||
D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicSearchSingleton.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>"; };
|
||||
|
|
@ -165,6 +171,7 @@
|
|||
D717071F2AC5989C0037746F /* TextExtension.swift */,
|
||||
D74C9D002ACB098C0021626A /* PermissionManager.swift */,
|
||||
D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */,
|
||||
D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -331,6 +338,7 @@
|
|||
D7E6D0542AEBFCCE00A57AAF /* ContactsInnerFragment.swift */,
|
||||
D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */,
|
||||
D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */,
|
||||
D7C365092AF001C300FE6142 /* EditContactFragment.swift */,
|
||||
);
|
||||
path = Fragments;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -341,6 +349,7 @@
|
|||
D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */,
|
||||
D71FCA7E2AE1397200D2E43E /* ContactsListViewModel.swift */,
|
||||
D7E6D04A2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift */,
|
||||
D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */,
|
||||
);
|
||||
path = ViewModel;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -541,15 +550,18 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */,
|
||||
D71707202AC5989C0037746F /* TextExtension.swift in Sources */,
|
||||
D719ABB92ABC67BF00B41C10 /* ContentView.swift in Sources */,
|
||||
D71FCA832AE14D6E00D2E43E /* ContactFragment.swift in Sources */,
|
||||
D7C3650C2AF0084000FE6142 /* EditContactViewModel.swift in Sources */,
|
||||
D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */,
|
||||
D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */,
|
||||
D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */,
|
||||
D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */,
|
||||
D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */,
|
||||
D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */,
|
||||
D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */,
|
||||
D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */,
|
||||
D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */,
|
||||
D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_3627_17791)">
|
||||
<rect x="0" y="0" width="120" height="120" rx="60" fill="#DFECF2"/>
|
||||
<path d="M59.9666 26.667C41.5666 26.667 26.6666 41.6003 26.6666 60.0003C26.6666 78.4003 41.5666 93.3337 59.9666 93.3337C78.4 93.3337 93.3333 78.4003 93.3333 60.0003C93.3333 41.6003 78.4 26.667 59.9666 26.667ZM60 86.667C45.2666 86.667 33.3333 74.7337 33.3333 60.0003C33.3333 45.267 45.2666 33.3337 60 33.3337C74.7333 33.3337 86.6666 45.267 86.6666 60.0003C86.6666 74.7337 74.7333 86.667 60 86.667ZM71.6666 56.667C74.4333 56.667 76.6666 54.4337 76.6666 51.667C76.6666 48.9003 74.4333 46.667 71.6666 46.667C68.9 46.667 66.6666 48.9003 66.6666 51.667C66.6666 54.4337 68.9 56.667 71.6666 56.667ZM48.3333 56.667C51.1 56.667 53.3333 54.4337 53.3333 51.667C53.3333 48.9003 51.1 46.667 48.3333 46.667C45.5666 46.667 43.3333 48.9003 43.3333 51.667C43.3333 54.4337 45.5666 56.667 48.3333 56.667ZM60 78.3337C67.7666 78.3337 74.3666 73.467 77.0333 66.667H42.9666C45.6333 73.467 52.2333 78.3337 60 78.3337Z" fill="#4E6074"/>
|
||||
<rect x="0" y="0" width="256" height="256" rx="128" fill="#DFECF2"/>
|
||||
<path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24ZM74.08,197.5a64,64,0,0,1,107.84,0,87.83,87.83,0,0,1-107.84,0ZM96,120a32,32,0,1,1,32,32A32,32,0,0,1,96,120Zm97.76,66.41a79.66,79.66,0,0,0-36.06-28.75,48,48,0,1,0-59.4,0,79.66,79.66,0,0,0-36.06,28.75,88,88,0,1,1,131.52,0Z" fill="#4E6074"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_3627_17791" x="0" y="0" width="120" height="120" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<filter id="filter0_d_3627_17791" x="0" y="0" width="256" height="256" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.2 KiB |
|
|
@ -22,55 +22,76 @@ import Contacts
|
|||
import SwiftUI
|
||||
|
||||
final class ContactsManager: ObservableObject {
|
||||
|
||||
static let shared = ContactsManager()
|
||||
|
||||
private var coreContext = CoreContext.shared
|
||||
private var magicSearch = MagicSearchSingleton.shared
|
||||
|
||||
private let nativeAddressBookFriendList = "Native address-book"
|
||||
let linphoneAddressBookFirendList = "Linphone address-book"
|
||||
static let shared = ContactsManager()
|
||||
|
||||
private var coreContext = CoreContext.shared
|
||||
private var magicSearch = MagicSearchSingleton.shared
|
||||
|
||||
private let nativeAddressBookFriendList = "Native address-book"
|
||||
let linphoneAddressBookFirendList = "Linphone address-book"
|
||||
|
||||
@Published var friendList: FriendList?
|
||||
|
||||
private init() {
|
||||
fetchContacts()
|
||||
}
|
||||
|
||||
func fetchContacts() {
|
||||
DispatchQueue.global().async {
|
||||
if self.coreContext.mCore.globalState == GlobalState.Shutdown || self.coreContext.mCore.globalState == GlobalState.Off {
|
||||
print("$TAG Core is being stopped or already destroyed, abort")
|
||||
@Published var linphoneFriendList: FriendList?
|
||||
|
||||
private init() {
|
||||
fetchContacts()
|
||||
}
|
||||
|
||||
func fetchContacts() {
|
||||
DispatchQueue.global().async {
|
||||
if self.coreContext.mCore.globalState == GlobalState.Shutdown || self.coreContext.mCore.globalState == GlobalState.Off {
|
||||
print("$TAG Core is being stopped or already destroyed, abort")
|
||||
} else {
|
||||
print("$TAG ${friends.size} friends created")
|
||||
|
||||
print("$TAG ${friends.size} friends created")
|
||||
|
||||
self.friendList = self.coreContext.mCore.getFriendListByName(name: self.nativeAddressBookFriendList)
|
||||
if self.friendList == nil {
|
||||
do {
|
||||
do {
|
||||
self.friendList = try self.coreContext.mCore.createFriendList()
|
||||
} catch let error {
|
||||
print("Failed to enumerate contact", error)
|
||||
}
|
||||
}
|
||||
|
||||
} catch let error {
|
||||
print("Failed to enumerate contact", error)
|
||||
}
|
||||
}
|
||||
|
||||
if self.friendList!.displayName == nil || self.friendList!.displayName!.isEmpty {
|
||||
print(
|
||||
"$TAG Friend list [$nativeAddressBookFriendList] didn't exist yet, let's create it"
|
||||
)
|
||||
|
||||
print(
|
||||
"$TAG Friend list [$nativeAddressBookFriendList] didn't exist yet, let's create it"
|
||||
)
|
||||
|
||||
self.friendList!.databaseStorageEnabled = false // We don't want to store local address-book in DB
|
||||
|
||||
|
||||
self.friendList!.displayName = self.nativeAddressBookFriendList
|
||||
self.coreContext.mCore.addFriendList(list: self.friendList!)
|
||||
} else {
|
||||
print(
|
||||
"$TAG Friend list [$LINPHONE_ADDRESS_BOOK_FRIEND_LIST] found, removing existing friends if any"
|
||||
)
|
||||
} else {
|
||||
print(
|
||||
"$TAG Friend list [$LINPHONE_ADDRESS_BOOK_FRIEND_LIST] found, removing existing friends if any"
|
||||
)
|
||||
self.friendList!.friends.forEach { friend in
|
||||
_ = self.friendList!.removeFriend(linphoneFriend: friend)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.linphoneFriendList = self.coreContext.mCore.getFriendListByName(name: self.linphoneAddressBookFirendList)
|
||||
if self.linphoneFriendList == nil {
|
||||
do {
|
||||
self.linphoneFriendList = try self.coreContext.mCore.createFriendList()
|
||||
} catch let error {
|
||||
print("Failed to enumerate contact", error)
|
||||
}
|
||||
}
|
||||
|
||||
if self.linphoneFriendList!.displayName == nil || self.linphoneFriendList!.displayName!.isEmpty {
|
||||
print(
|
||||
"$TAG Friend list [$linphoneAddressBookFirendList] didn't exist yet, let's create it"
|
||||
)
|
||||
|
||||
self.linphoneFriendList!.databaseStorageEnabled = true
|
||||
|
||||
self.linphoneFriendList!.displayName = self.linphoneAddressBookFirendList
|
||||
self.coreContext.mCore.addFriendList(list: self.linphoneFriendList!)
|
||||
}
|
||||
}
|
||||
|
||||
let store = CNContactStore()
|
||||
store.requestAccess(for: .contacts) { (granted, error) in
|
||||
|
|
@ -87,29 +108,29 @@ final class ContactsManager: ObservableObject {
|
|||
let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
|
||||
do {
|
||||
try store.enumerateContacts(with: request, usingBlock: { (contact, _) in
|
||||
|
||||
DispatchQueue.main.sync {
|
||||
let newContact = Contact(
|
||||
firstName: contact.givenName,
|
||||
lastName: contact.familyName,
|
||||
organizationName: contact.organizationName,
|
||||
displayName: contact.nickname,
|
||||
sipAddresses: contact.instantMessageAddresses.map { $0.value.service == "SIP" ? $0.value.username : "" },
|
||||
phoneNumbers: contact.phoneNumbers.map { PhoneNumber(numLabel: $0.label ?? "", num: $0.value.stringValue)},
|
||||
imageData: ""
|
||||
)
|
||||
firstName: contact.givenName,
|
||||
lastName: contact.familyName,
|
||||
organizationName: contact.organizationName,
|
||||
jobTitle: "",
|
||||
displayName: contact.nickname,
|
||||
sipAddresses: contact.instantMessageAddresses.map { $0.value.service == "SIP" ? $0.value.username : "" },
|
||||
phoneNumbers: contact.phoneNumbers.map { PhoneNumber(numLabel: $0.label ?? "", num: $0.value.stringValue)},
|
||||
imageData: ""
|
||||
)
|
||||
|
||||
let imageThumbnail = UIImage(data: contact.thumbnailImageData ?? Data())
|
||||
self.saveImage(
|
||||
image:
|
||||
UIImage(data: contact.thumbnailImageData ?? Data())
|
||||
image: imageThumbnail
|
||||
?? self.textToImage(
|
||||
firstName: contact.givenName.isEmpty
|
||||
&& contact.familyName.isEmpty
|
||||
&& contact.phoneNumbers.first?.value.stringValue != nil
|
||||
? contact.phoneNumbers.first!.value.stringValue
|
||||
: contact.givenName, lastName: contact.familyName),
|
||||
name: contact.givenName + contact.familyName + String(Int.random(in: 1...1000)),
|
||||
contact: newContact)
|
||||
&& contact.familyName.isEmpty
|
||||
&& contact.phoneNumbers.first?.value.stringValue != nil
|
||||
? contact.phoneNumbers.first!.value.stringValue
|
||||
: contact.givenName, lastName: contact.familyName),
|
||||
name: contact.givenName + contact.familyName + String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""),
|
||||
contact: newContact, linphoneFriend: false, existingFriend: nil)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -121,9 +142,9 @@ final class ContactsManager: ObservableObject {
|
|||
print("access denied")
|
||||
}
|
||||
}
|
||||
self.magicSearch.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
|
||||
}
|
||||
}
|
||||
self.magicSearch.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
func textToImage(firstName: String, lastName: String) -> UIImage {
|
||||
let lblNameInitialize = UILabel()
|
||||
|
|
@ -152,36 +173,70 @@ final class ContactsManager: ObservableObject {
|
|||
|
||||
return IBImgViewUserProfile
|
||||
}
|
||||
|
||||
func saveImage(image: UIImage, name: String, contact: Contact) {
|
||||
guard let data = image.jpegData(compressionQuality: 1) ?? image.pngData() else {
|
||||
return
|
||||
}
|
||||
|
||||
func saveImage(image: UIImage, name: String, contact: Contact, linphoneFriend: Bool, existingFriend: Friend?) {
|
||||
guard let data = image.jpegData(compressionQuality: 1) ?? image.pngData() else {
|
||||
return
|
||||
}
|
||||
|
||||
awaitDataWrite(data: data, name: name) { _, result in
|
||||
do {
|
||||
let friend = try self.coreContext.mCore.createFriend()
|
||||
friend.edit()
|
||||
try friend.setName(newValue: contact.firstName + " " + contact.lastName)
|
||||
friend.organization = contact.organizationName
|
||||
let resultFriend = self.saveFriend(result: result, contact: contact, existingFriend: existingFriend)
|
||||
|
||||
if resultFriend != nil {
|
||||
if linphoneFriend && existingFriend == nil {
|
||||
_ = self.linphoneFriendList!.addLocalFriend(linphoneFriend: resultFriend!)
|
||||
|
||||
self.linphoneFriendList!.updateSubscriptions()
|
||||
} else if existingFriend == nil {
|
||||
_ = self.friendList!.addLocalFriend(linphoneFriend: resultFriend!)
|
||||
|
||||
self.friendList!.updateSubscriptions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveFriend(result: String, contact: Contact, existingFriend: Friend?) -> Friend? {
|
||||
do {
|
||||
let friend = (existingFriend != nil) ? existingFriend : try self.coreContext.mCore.createFriend()
|
||||
|
||||
if friend != nil {
|
||||
friend!.edit()
|
||||
|
||||
try friend!.setName(newValue: contact.firstName + " " + contact.lastName)
|
||||
|
||||
let friendvCard = friend!.vcard
|
||||
|
||||
if friendvCard != nil {
|
||||
friendvCard!.givenName = contact.firstName
|
||||
friendvCard!.familyName = contact.lastName
|
||||
}
|
||||
|
||||
friend!.organization = contact.organizationName
|
||||
|
||||
var friendAddresses: [Address] = []
|
||||
friend?.addresses.forEach({ address in
|
||||
friend?.removeAddress(address: address)
|
||||
})
|
||||
contact.sipAddresses.forEach { sipAddress in
|
||||
let address = self.coreContext.mCore.interpretUrl(url: sipAddress, applyInternationalPrefix: true)
|
||||
|
||||
if address != nil && ((friendAddresses.firstIndex(where: {$0.asString() == address?.asString()})) == nil) {
|
||||
friend.addAddress(address: address!)
|
||||
friend!.addAddress(address: address!)
|
||||
friendAddresses.append(address!)
|
||||
}
|
||||
}
|
||||
|
||||
var friendPhoneNumbers: [PhoneNumber] = []
|
||||
friend?.phoneNumbersWithLabel.forEach({ phoneNumber in
|
||||
friend?.removePhoneNumberWithLabel(phoneNumber: phoneNumber)
|
||||
})
|
||||
contact.phoneNumbers.forEach { phone in
|
||||
do {
|
||||
if (friendPhoneNumbers.firstIndex(where: {$0.numLabel == phone.numLabel})) == nil {
|
||||
let labelDrop = String(phone.numLabel.dropFirst(4).dropLast(4))
|
||||
let phoneNumber = try Factory.Instance.createFriendPhoneNumber(phoneNumber: phone.num, label: labelDrop)
|
||||
friend.addPhoneNumberWithLabel(phoneNumber: phoneNumber)
|
||||
if (friendPhoneNumbers.firstIndex(where: {$0.num == phone.num})) == nil {
|
||||
let labelDrop = String(phone.numLabel.dropFirst(4).dropLast(4))
|
||||
let phoneNumber = try Factory.Instance.createFriendPhoneNumber(phoneNumber: phone.num, label: labelDrop)
|
||||
friend!.addPhoneNumberWithLabel(phoneNumber: phoneNumber)
|
||||
friendPhoneNumbers.append(phone)
|
||||
}
|
||||
} catch let error {
|
||||
|
|
@ -189,50 +244,61 @@ final class ContactsManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
let contactImage = result.dropFirst(8)
|
||||
friend.photo = "file:/" + contactImage
|
||||
|
||||
friend.organization = contact.organizationName
|
||||
friend!.photo = "file:/" + result
|
||||
|
||||
friend.done()
|
||||
friend!.organization = contact.organizationName
|
||||
friend!.jobTitle = contact.jobTitle
|
||||
|
||||
_ = self.friendList!.addLocalFriend(linphoneFriend: friend)
|
||||
|
||||
self.friendList!.updateSubscriptions()
|
||||
|
||||
} catch let error {
|
||||
print("Failed to enumerate contact", error)
|
||||
friend!.done()
|
||||
return friend
|
||||
}
|
||||
} catch let error {
|
||||
print("Failed to enumerate contact", error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getImagePath(friendPhotoPath: String) -> URL {
|
||||
let friendPath = String(friendPhotoPath.dropFirst(6))
|
||||
|
||||
let imagePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(friendPath)
|
||||
|
||||
return imagePath
|
||||
}
|
||||
|
||||
func awaitDataWrite(data: Data, name: String, completion: @escaping ((), String) -> Void) {
|
||||
let directory = FileManager.default.temporaryDirectory
|
||||
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if directory != nil {
|
||||
DispatchQueue.main.async {
|
||||
do {
|
||||
let decodedData: () = try data.write(to: directory.appendingPathComponent(name + ".png"))
|
||||
completion(decodedData, directory.appendingPathComponent(name + ".png").absoluteString) // <--- here, return the results
|
||||
let urlName = URL(string: name)
|
||||
let imagePath = urlName != nil ? urlName!.absoluteString.replacingOccurrences(of: "%", with: "") : String(Int.random(in: 1...1000))
|
||||
let decodedData: () = try data.write(to: directory!.appendingPathComponent(imagePath + ".png"))
|
||||
completion(decodedData, imagePath + ".png")
|
||||
} catch {
|
||||
print("Error: ", error) // need to deal with errors
|
||||
completion((), "") // <--- here, should return the error
|
||||
print("Error: ", error)
|
||||
completion((), "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PhoneNumber {
|
||||
var numLabel: String
|
||||
var num: String
|
||||
var numLabel: String
|
||||
var num: String
|
||||
}
|
||||
|
||||
struct Contact: Identifiable {
|
||||
var id = UUID()
|
||||
var firstName: String
|
||||
var lastName: String
|
||||
var organizationName: String
|
||||
var displayName: String
|
||||
var sipAddresses: [String] = []
|
||||
var phoneNumbers: [PhoneNumber] = []
|
||||
var imageData: String
|
||||
var id = UUID()
|
||||
var firstName: String
|
||||
var lastName: String
|
||||
var organizationName: String
|
||||
var jobTitle: String
|
||||
var displayName: String
|
||||
var sipAddresses: [String] = []
|
||||
var phoneNumbers: [PhoneNumber] = []
|
||||
var imageData: String
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ struct LinphoneApp: App {
|
|||
AssistantView(sharedMainViewModel: sharedMainViewModel)
|
||||
.toast(isShowing: $coreContext.toastMessage)
|
||||
} else if coreContext.mCore.defaultAccount != nil {
|
||||
ContentView(contactViewModel: ContactViewModel(), historyViewModel: HistoryViewModel())
|
||||
ContentView(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), historyViewModel: HistoryViewModel())
|
||||
.toast(isShowing: $coreContext.toastMessage)
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@
|
|||
},
|
||||
"**Contacts** : Pour vous afficher vos contacts et retrouver qui utilise Linphone." : {
|
||||
|
||||
},
|
||||
"**Job :** %@" : {
|
||||
|
||||
},
|
||||
"**Micro** : Pour permettre à vos correspondants de vous entendre." : {
|
||||
|
||||
|
|
@ -95,12 +98,18 @@
|
|||
},
|
||||
"Accept all" : {
|
||||
|
||||
},
|
||||
"Add a picture" : {
|
||||
|
||||
},
|
||||
"Add to favourites" : {
|
||||
|
||||
},
|
||||
"All contacts" : {
|
||||
|
||||
},
|
||||
"All modifications will be canceled." : {
|
||||
|
||||
},
|
||||
"Appel" : {
|
||||
|
||||
|
|
@ -121,9 +130,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Block" : {
|
||||
|
||||
},
|
||||
"Block the address" : {
|
||||
|
||||
|
|
@ -145,6 +151,9 @@
|
|||
},
|
||||
"Close" : {
|
||||
|
||||
},
|
||||
"Company" : {
|
||||
|
||||
},
|
||||
"Conditions de service" : {
|
||||
|
||||
|
|
@ -190,9 +199,18 @@
|
|||
},
|
||||
"Domain" : {
|
||||
|
||||
},
|
||||
"Don’t save modifications?" : {
|
||||
|
||||
},
|
||||
"Edit" : {
|
||||
|
||||
},
|
||||
"Edit contact" : {
|
||||
|
||||
},
|
||||
"Edit picture" : {
|
||||
|
||||
},
|
||||
"En continuant, vous acceptez ces conditions, " : {
|
||||
|
||||
|
|
@ -208,6 +226,12 @@
|
|||
},
|
||||
"Favourites" : {
|
||||
|
||||
},
|
||||
"First Name" : {
|
||||
|
||||
},
|
||||
"First name*" : {
|
||||
|
||||
},
|
||||
"History Contact fragment" : {
|
||||
|
||||
|
|
@ -235,6 +259,12 @@
|
|||
},
|
||||
"Invitation" : {
|
||||
|
||||
},
|
||||
"Job title" : {
|
||||
|
||||
},
|
||||
"Last name" : {
|
||||
|
||||
},
|
||||
"Linphone" : {
|
||||
|
||||
|
|
@ -248,10 +278,10 @@
|
|||
"Message" : {
|
||||
|
||||
},
|
||||
"Mute" : {
|
||||
"My Profile" : {
|
||||
|
||||
},
|
||||
"My Profile" : {
|
||||
"New contact" : {
|
||||
|
||||
},
|
||||
"Next" : {
|
||||
|
|
@ -297,9 +327,15 @@
|
|||
},
|
||||
"Personnalize your profil mode" : {
|
||||
|
||||
},
|
||||
"Phone :" : {
|
||||
|
||||
},
|
||||
"Phone (%@) :" : {
|
||||
|
||||
},
|
||||
"Phone number" : {
|
||||
|
||||
},
|
||||
"Plus tard" : {
|
||||
|
||||
|
|
@ -316,7 +352,10 @@
|
|||
"Register" : {
|
||||
|
||||
},
|
||||
"Remove to favourites" : {
|
||||
"Remove from favourites" : {
|
||||
|
||||
},
|
||||
"Remove picture" : {
|
||||
|
||||
},
|
||||
"Scan QR code" : {
|
||||
|
|
@ -333,6 +372,9 @@
|
|||
},
|
||||
"Share" : {
|
||||
|
||||
},
|
||||
"SIP address" : {
|
||||
|
||||
},
|
||||
"SIP address :" : {
|
||||
|
||||
|
|
|
|||
|
|
@ -23,17 +23,22 @@ struct ContactsView: View {
|
|||
|
||||
@ObservedObject var contactViewModel: ContactViewModel
|
||||
@ObservedObject var historyViewModel: HistoryViewModel
|
||||
@ObservedObject var editContactViewModel: EditContactViewModel
|
||||
|
||||
@Binding var isShowEditContactFragment: Bool
|
||||
@Binding var isShowDeletePopup: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
|
||||
ContactsFragment(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup)
|
||||
|
||||
Button {
|
||||
// Action
|
||||
withAnimation {
|
||||
editContactViewModel.selectedEditFriend = nil
|
||||
editContactViewModel.resetValues()
|
||||
isShowEditContactFragment.toggle()
|
||||
}
|
||||
} label: {
|
||||
Image("user-plus")
|
||||
.padding()
|
||||
|
|
@ -50,5 +55,11 @@ struct ContactsView: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
ContactsView(contactViewModel: ContactViewModel(), historyViewModel: HistoryViewModel(), isShowDeletePopup: .constant(false))
|
||||
ContactsView(
|
||||
contactViewModel: ContactViewModel(),
|
||||
historyViewModel: HistoryViewModel(),
|
||||
editContactViewModel: EditContactViewModel(),
|
||||
isShowEditContactFragment: .constant(false),
|
||||
isShowDeletePopup: .constant(false)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,21 +21,50 @@ import SwiftUI
|
|||
|
||||
struct ContactFragment: View {
|
||||
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
|
||||
@ObservedObject var contactViewModel: ContactViewModel
|
||||
@ObservedObject var editContactViewModel: EditContactViewModel
|
||||
|
||||
@Binding var isShowDeletePopup: Bool
|
||||
@Binding var isShowDismissPopup: Bool
|
||||
|
||||
@State private var showingSheet = false
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
ContactInnerFragment(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
|
||||
.sheet(isPresented: $showingSheet) {
|
||||
ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet)
|
||||
.presentationDetents([.fraction(0.2)])
|
||||
}
|
||||
if idiom != .pad {
|
||||
ContactInnerFragment(
|
||||
contactViewModel: contactViewModel,
|
||||
editContactViewModel: editContactViewModel,
|
||||
isShowDeletePopup: $isShowDeletePopup,
|
||||
showingSheet: $showingSheet,
|
||||
isShowDismissPopup: $isShowDismissPopup
|
||||
)
|
||||
.sheet(isPresented: $showingSheet) {
|
||||
ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet)
|
||||
.presentationDetents([.fraction(0.2)])
|
||||
}
|
||||
} else {
|
||||
ContactInnerFragment(
|
||||
contactViewModel: contactViewModel,
|
||||
editContactViewModel: editContactViewModel,
|
||||
isShowDeletePopup: $isShowDeletePopup,
|
||||
showingSheet: $showingSheet,
|
||||
isShowDismissPopup: $isShowDismissPopup
|
||||
)
|
||||
.halfSheet(showSheet: $showingSheet) {
|
||||
ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet)
|
||||
} onDismiss: {}
|
||||
}
|
||||
} else {
|
||||
ContactInnerFragment(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
|
||||
ContactInnerFragment(
|
||||
contactViewModel: contactViewModel,
|
||||
editContactViewModel: editContactViewModel,
|
||||
isShowDeletePopup: $isShowDeletePopup,
|
||||
showingSheet: $showingSheet,
|
||||
isShowDismissPopup: $isShowDismissPopup
|
||||
)
|
||||
.halfSheet(showSheet: $showingSheet) {
|
||||
ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet)
|
||||
} onDismiss: {}
|
||||
|
|
@ -45,5 +74,5 @@ struct ContactFragment: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
ContactFragment(contactViewModel: ContactViewModel(), isShowDeletePopup: .constant(false))
|
||||
ContactFragment(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), isShowDeletePopup: .constant(false), isShowDismissPopup: .constant(false))
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -22,6 +22,8 @@ import UniformTypeIdentifiers
|
|||
|
||||
struct ContactListBottomSheet: View {
|
||||
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
|
||||
@ObservedObject var magicSearch = MagicSearchSingleton.shared
|
||||
|
||||
@ObservedObject var contactViewModel: ContactViewModel
|
||||
|
|
@ -34,9 +36,9 @@ struct ContactListBottomSheet: View {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if orientation == .landscapeLeft
|
||||
if idiom != .pad && (orientation == .landscapeLeft
|
||||
|| orientation == .landscapeRight
|
||||
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
|
||||
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
|
|
|
|||
|
|
@ -20,21 +20,30 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ContactsFragment: View {
|
||||
|
||||
@ObservedObject var contactViewModel: ContactViewModel
|
||||
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
|
||||
@ObservedObject var contactViewModel: ContactViewModel
|
||||
|
||||
@Binding var isShowDeletePopup: Bool
|
||||
|
||||
@State private var showingSheet = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
@State private var showingSheet = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if #available(iOS 16.0, *) {
|
||||
ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet)
|
||||
.sheet(isPresented: $showingSheet) {
|
||||
ContactsListBottomSheet(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
|
||||
.presentationDetents([.fraction(0.2)])
|
||||
}
|
||||
if idiom != .pad {
|
||||
ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet)
|
||||
.sheet(isPresented: $showingSheet) {
|
||||
ContactsListBottomSheet(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
|
||||
.presentationDetents([.fraction(0.2)])
|
||||
}
|
||||
} else {
|
||||
ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet)
|
||||
.halfSheet(showSheet: $showingSheet) {
|
||||
ContactsListBottomSheet(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup, showingSheet: $showingSheet)
|
||||
} onDismiss: {}
|
||||
}
|
||||
} else {
|
||||
ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet)
|
||||
.halfSheet(showSheet: $showingSheet) {
|
||||
|
|
@ -42,7 +51,7 @@ struct ContactsFragment: View {
|
|||
} onDismiss: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ import SwiftUI
|
|||
import linphonesw
|
||||
|
||||
struct ContactsListBottomSheet: View {
|
||||
|
||||
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
|
||||
@ObservedObject var magicSearch = MagicSearchSingleton.shared
|
||||
|
||||
@ObservedObject var contactViewModel: ContactViewModel
|
||||
|
|
@ -36,9 +38,9 @@ struct ContactsListBottomSheet: View {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if orientation == .landscapeLeft
|
||||
if idiom != .pad && (orientation == .landscapeLeft
|
||||
|| orientation == .landscapeRight
|
||||
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
|
||||
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
|
|
@ -72,10 +74,10 @@ struct ContactsListBottomSheet: View {
|
|||
Image(contactViewModel.selectedFriend != nil && contactViewModel.selectedFriend!.starred == true ? "heart-fill" : "heart")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.foregroundStyle(contactViewModel.selectedFriend != nil && contactViewModel.selectedFriend!.starred == true ? Color.redDanger500 : Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
Text(contactViewModel.selectedFriend != nil && contactViewModel.selectedFriend!.starred == true
|
||||
? "Remove to favourites"
|
||||
? "Remove from favourites"
|
||||
: "Add to favourites")
|
||||
.default_text_style(styleSize: 16)
|
||||
Spacer()
|
||||
|
|
@ -147,10 +149,13 @@ struct ContactsListBottomSheet: View {
|
|||
.background(Color.gray100)
|
||||
|
||||
}
|
||||
.background(Color.gray100)
|
||||
.frame(maxWidth: .infinity)
|
||||
.onRotate { newOrientation in
|
||||
orientation = newOrientation
|
||||
}
|
||||
.background(Color.gray100)
|
||||
.frame(maxWidth: .infinity)
|
||||
.onDisappear {
|
||||
contactViewModel.selectedFriend = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ struct ContactsListFragment: View {
|
|||
}
|
||||
|
||||
if magicSearch.lastSearch[index].friend!.photo != nil && !magicSearch.lastSearch[index].friend!.photo!.isEmpty {
|
||||
AsyncImage(url: URL(string: magicSearch.lastSearch[index].friend!.photo!)) { image in
|
||||
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: magicSearch.lastSearch[index].friend!.photo!)) { image in
|
||||
switch image {
|
||||
case .empty:
|
||||
ProgressView()
|
||||
|
|
@ -106,7 +106,7 @@ struct ContactsListFragment: View {
|
|||
TapGesture()
|
||||
.onEnded { _ in
|
||||
withAnimation {
|
||||
contactViewModel.displayedFriend = magicSearch.lastSearch[index].friend
|
||||
contactViewModel.indexDisplayedFriend = index
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
517
Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift
Normal file
517
Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
/*
|
||||
* 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 EditContactFragment: View {
|
||||
|
||||
@ObservedObject var editContactViewModel: EditContactViewModel
|
||||
@ObservedObject private var sharedMainViewModel = SharedMainViewModel()
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
@Binding var isShowEditContactFragment: Bool
|
||||
@Binding var isShowDismissPopup: Bool
|
||||
|
||||
@State private var hasTimeElapsed = false
|
||||
@State private var delayedColor = Color.white
|
||||
|
||||
@FocusState var isFirstNameFocused: Bool
|
||||
@FocusState var isLastNameFocused: Bool
|
||||
@FocusState var isSIPAddressFocused: Int?
|
||||
@FocusState var isPhoneNumberFocused: Int?
|
||||
@FocusState var isCompanyFocused: Bool
|
||||
@FocusState var isJobTitleFocused: Bool
|
||||
|
||||
@State private var showPhotoPicker = false
|
||||
@State private var selectedImage: UIImage?
|
||||
@State private var removedImage = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack(spacing: 1) {
|
||||
if editContactViewModel.selectedEditFriend == nil {
|
||||
Rectangle()
|
||||
.foregroundColor(delayedColor)
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.frame(height: 0)
|
||||
.task(delayColor)
|
||||
} else {
|
||||
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(.top, 2)
|
||||
.onTapGesture {
|
||||
if editContactViewModel.selectedEditFriend == nil
|
||||
&& editContactViewModel.firstName.isEmpty
|
||||
&& editContactViewModel.lastName.isEmpty
|
||||
&& editContactViewModel.sipAddresses.first!.isEmpty
|
||||
&& editContactViewModel.phoneNumbers.first!.isEmpty
|
||||
&& editContactViewModel.company.isEmpty
|
||||
&& editContactViewModel.jobTitle.isEmpty {
|
||||
delayColorDismiss()
|
||||
withAnimation {
|
||||
isShowEditContactFragment.toggle()
|
||||
}
|
||||
} else if editContactViewModel.selectedEditFriend == nil {
|
||||
isShowDismissPopup.toggle()
|
||||
} else {
|
||||
if editContactViewModel.firstName.isEmpty
|
||||
&& editContactViewModel.lastName.isEmpty
|
||||
&& editContactViewModel.sipAddresses.first!.isEmpty
|
||||
&& editContactViewModel.phoneNumbers.first!.isEmpty
|
||||
&& editContactViewModel.company.isEmpty
|
||||
&& editContactViewModel.jobTitle.isEmpty {
|
||||
withAnimation {
|
||||
dismiss()
|
||||
}
|
||||
} else {
|
||||
isShowDismissPopup.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(editContactViewModel.selectedEditFriend == nil ? "New contact" : "Edit contact")
|
||||
.multilineTextAlignment(.leading)
|
||||
.default_text_style_orange_800(styleSize: 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("check")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(editContactViewModel.firstName.isEmpty ? Color.orangeMain100 : Color.orangeMain500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.top, 2)
|
||||
.disabled(editContactViewModel.firstName.isEmpty)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
addOrEditFriend()
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 4)
|
||||
.background(.white)
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
if editContactViewModel.selectedEditFriend != nil
|
||||
&& editContactViewModel.selectedEditFriend!.photo != nil
|
||||
&& !editContactViewModel.selectedEditFriend!.photo!.isEmpty && selectedImage == nil && !removedImage {
|
||||
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: editContactViewModel.selectedEditFriend!.photo!)) { image in
|
||||
switch image {
|
||||
case .empty:
|
||||
ProgressView()
|
||||
.frame(width: 100, height: 100)
|
||||
case .success(let image):
|
||||
image
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(Circle())
|
||||
case .failure:
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(Circle())
|
||||
@unknown default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
} else if selectedImage == nil {
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Image(uiImage: selectedImage!)
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
if editContactViewModel.selectedEditFriend != nil
|
||||
&& editContactViewModel.selectedEditFriend!.photo != nil
|
||||
&& !editContactViewModel.selectedEditFriend!.photo!.isEmpty
|
||||
&& (editContactViewModel.selectedEditFriend!.photo!.suffix(11) != "default.png" || selectedImage != nil) && !removedImage {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
showPhotoPicker = true
|
||||
}, label: {
|
||||
HStack {
|
||||
Image("pencil-simple")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Text("Edit picture")
|
||||
.foregroundStyle(Color.grayMain2c700)
|
||||
.multilineTextAlignment(.center)
|
||||
.default_text_style(styleSize: 14)
|
||||
}
|
||||
})
|
||||
.padding(.top, 10)
|
||||
.padding(.trailing, 10)
|
||||
.sheet(isPresented: $showPhotoPicker) {
|
||||
PhotoPicker(filter: .images, limit: 1) { results in
|
||||
PhotoPicker.convertToUIImageArray(fromResults: results) { imagesOrNil, errorOrNil in
|
||||
if let error = errorOrNil {
|
||||
print(error)
|
||||
}
|
||||
if let images = imagesOrNil {
|
||||
if let first = images.first {
|
||||
selectedImage = first
|
||||
removedImage = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
removedImage = true
|
||||
selectedImage = nil
|
||||
}, label: {
|
||||
HStack {
|
||||
Image("trash-simple")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Text("Remove picture")
|
||||
.foregroundStyle(Color.grayMain2c700)
|
||||
.multilineTextAlignment(.center)
|
||||
.default_text_style(styleSize: 14)
|
||||
}
|
||||
})
|
||||
.padding(.top, 10)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
} else {
|
||||
Button(action: {
|
||||
showPhotoPicker = true
|
||||
}, label: {
|
||||
HStack {
|
||||
Image("camera")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Text("Add a picture")
|
||||
.foregroundStyle(Color.grayMain2c700)
|
||||
.multilineTextAlignment(.center)
|
||||
.default_text_style(styleSize: 14)
|
||||
}
|
||||
})
|
||||
.padding(.top, 10)
|
||||
.sheet(isPresented: $showPhotoPicker) {
|
||||
PhotoPicker(filter: .images, limit: 1) { results in
|
||||
PhotoPicker.convertToUIImageArray(fromResults: results) { imagesOrNil, errorOrNil in
|
||||
if let error = errorOrNil {
|
||||
print(error)
|
||||
}
|
||||
if let images = imagesOrNil {
|
||||
if let first = images.first {
|
||||
selectedImage = first
|
||||
removedImage = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 150)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 10)
|
||||
.background(Color.gray100)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("First name*")
|
||||
.default_text_style_700(styleSize: 15)
|
||||
.padding(.bottom, -5)
|
||||
|
||||
TextField("First Name", text: $editContactViewModel.firstName)
|
||||
.default_text_style(styleSize: 15)
|
||||
.frame(height: 25)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 15)
|
||||
.background(.white)
|
||||
.cornerRadius(60)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 60)
|
||||
.inset(by: 0.5)
|
||||
.stroke(isFirstNameFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
|
||||
)
|
||||
.padding(.bottom)
|
||||
.focused($isFirstNameFocused)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Last name")
|
||||
.default_text_style_700(styleSize: 15)
|
||||
.padding(.bottom, -5)
|
||||
|
||||
TextField("Last name", text: $editContactViewModel.lastName)
|
||||
.default_text_style(styleSize: 15)
|
||||
.frame(height: 25)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 15)
|
||||
.background(.white)
|
||||
.cornerRadius(60)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 60)
|
||||
.inset(by: 0.5)
|
||||
.stroke(isLastNameFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
|
||||
)
|
||||
.padding(.bottom)
|
||||
.focused($isLastNameFocused)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("SIP address")
|
||||
.default_text_style_700(styleSize: 15)
|
||||
.padding(.bottom, -5)
|
||||
|
||||
ForEach(0..<editContactViewModel.sipAddresses.count, id: \.self) { index in
|
||||
|
||||
HStack(alignment: .center) {
|
||||
TextField("SIP address", text: $editContactViewModel.sipAddresses[index])
|
||||
.default_text_style(styleSize: 15)
|
||||
.frame(height: 25)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 15)
|
||||
.background(.white)
|
||||
.cornerRadius(60)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 60)
|
||||
.inset(by: 0.5)
|
||||
.stroke(isSIPAddressFocused == index ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
|
||||
)
|
||||
.focused($isSIPAddressFocused, equals: index)
|
||||
.onChange(of: editContactViewModel.sipAddresses[index]) { newValue in
|
||||
if !newValue.isEmpty && index + 1 == editContactViewModel.sipAddresses.count {
|
||||
editContactViewModel.sipAddresses.append("")
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
editContactViewModel.sipAddresses.remove(at: index)
|
||||
}, label: {
|
||||
Image("x")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(editContactViewModel.sipAddresses[index].isEmpty && editContactViewModel.sipAddresses.count == index + 1 ? Color.gray100 : Color.grayMain2c600)
|
||||
.frame(width: 25, height: 25)
|
||||
})
|
||||
.disabled(editContactViewModel.sipAddresses[index].isEmpty && editContactViewModel.sipAddresses.count == index + 1)
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Phone number")
|
||||
.default_text_style_700(styleSize: 15)
|
||||
.padding(.bottom, -5)
|
||||
|
||||
ForEach(0..<editContactViewModel.phoneNumbers.count, id: \.self) { index in
|
||||
HStack(alignment: .center) {
|
||||
TextField("Phone number", text: $editContactViewModel.phoneNumbers[index])
|
||||
.default_text_style(styleSize: 15)
|
||||
.frame(height: 25)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 15)
|
||||
.background(.white)
|
||||
.cornerRadius(60)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 60)
|
||||
.inset(by: 0.5)
|
||||
.stroke(isPhoneNumberFocused == index ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
|
||||
)
|
||||
.focused($isPhoneNumberFocused, equals: index)
|
||||
.onChange(of: editContactViewModel.phoneNumbers[index]) { newValue in
|
||||
if !newValue.isEmpty && index + 1 == editContactViewModel.phoneNumbers.count {
|
||||
withAnimation {
|
||||
editContactViewModel.phoneNumbers.append("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
editContactViewModel.phoneNumbers.remove(at: index)
|
||||
}, label: {
|
||||
Image("x")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(editContactViewModel.phoneNumbers[index].isEmpty && editContactViewModel.phoneNumbers.count == index + 1 ? Color.gray100 : Color.grayMain2c600)
|
||||
.frame(width: 25, height: 25)
|
||||
})
|
||||
.disabled(editContactViewModel.phoneNumbers[index].isEmpty && editContactViewModel.phoneNumbers.count == index + 1)
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
.zIndex(isPhoneNumberFocused == index ? 1 : 0)
|
||||
.transition(.move(edge: .top))
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Company")
|
||||
.default_text_style_700(styleSize: 15)
|
||||
.padding(.bottom, -5)
|
||||
|
||||
TextField("Company", text: $editContactViewModel.company)
|
||||
.default_text_style(styleSize: 15)
|
||||
.frame(height: 25)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 15)
|
||||
.background(.white)
|
||||
.cornerRadius(60)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 60)
|
||||
.inset(by: 0.5)
|
||||
.stroke(isCompanyFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
|
||||
)
|
||||
.padding(.bottom)
|
||||
.focused($isCompanyFocused)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Job title")
|
||||
.default_text_style_700(styleSize: 15)
|
||||
.padding(.bottom, -5)
|
||||
|
||||
TextField("Job title", text: $editContactViewModel.jobTitle)
|
||||
.default_text_style(styleSize: 15)
|
||||
.frame(height: 25)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 15)
|
||||
.background(.white)
|
||||
.cornerRadius(60)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 60)
|
||||
.inset(by: 0.5)
|
||||
.stroke(isJobTitleFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
|
||||
)
|
||||
.padding(.bottom)
|
||||
.focused($isJobTitleFocused)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: sharedMainViewModel.maxWidth)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.background(Color.gray100)
|
||||
}
|
||||
.background(.white)
|
||||
|
||||
if editContactViewModel.removePopup {
|
||||
ZStack {
|
||||
|
||||
}.onAppear {
|
||||
if editContactViewModel.selectedEditFriend == nil {
|
||||
delayColorDismiss()
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
editContactViewModel.removePopup = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
|
||||
@Sendable private func delayColor() async {
|
||||
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||
delayedColor = Color.orangeMain500
|
||||
}
|
||||
|
||||
func delayColorDismiss() {
|
||||
if editContactViewModel.selectedEditFriend == nil {
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 80_000_000)
|
||||
delayedColor = .white
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addOrEditFriend() {
|
||||
let newContact = Contact(
|
||||
firstName: editContactViewModel.firstName,
|
||||
lastName: editContactViewModel.lastName,
|
||||
organizationName: editContactViewModel.company,
|
||||
jobTitle: editContactViewModel.jobTitle,
|
||||
displayName: "",
|
||||
sipAddresses: editContactViewModel.sipAddresses.map { $0 },
|
||||
phoneNumbers: editContactViewModel.phoneNumbers.map { PhoneNumber(numLabel: "", num: $0)},
|
||||
imageData: ""
|
||||
)
|
||||
|
||||
if editContactViewModel.selectedEditFriend != nil && selectedImage == nil &&
|
||||
!removedImage {
|
||||
let saveFriendResult = ContactsManager.shared.saveFriend(
|
||||
result: String(editContactViewModel.selectedEditFriend!.photo!.dropFirst(6)),
|
||||
contact: newContact,
|
||||
existingFriend: editContactViewModel.selectedEditFriend
|
||||
)
|
||||
} else {
|
||||
ContactsManager.shared.saveImage(
|
||||
image: selectedImage
|
||||
?? ContactsManager.shared.textToImage(
|
||||
firstName: editContactViewModel.firstName, lastName: editContactViewModel.lastName),
|
||||
name: editContactViewModel.firstName + editContactViewModel.lastName + String(Int.random(in: 1...1000)) + ((selectedImage == nil) ? "-default" : ""),
|
||||
contact: newContact, linphoneFriend: true, existingFriend: editContactViewModel.selectedEditFriend)
|
||||
}
|
||||
|
||||
MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
|
||||
|
||||
delayColorDismiss()
|
||||
if editContactViewModel.selectedEditFriend == nil {
|
||||
withAnimation {
|
||||
isShowEditContactFragment.toggle()
|
||||
}
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
editContactViewModel.resetValues()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
EditContactFragment(editContactViewModel: EditContactViewModel(), isShowEditContactFragment: .constant(false), isShowDismissPopup: .constant(false))
|
||||
}
|
||||
|
|
@ -32,62 +32,61 @@ struct FavoriteContactsListFragment: View {
|
|||
var body: some View {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
ForEach(0..<magicSearch.lastSearch.filter({ $0.friend?.starred == true }).count, id: \.self) { index in
|
||||
Button {
|
||||
} label: {
|
||||
VStack {
|
||||
if magicSearch.lastSearch.filter({ $0.friend?.starred == true })[index].friend!.photo != nil
|
||||
&& !magicSearch.lastSearch.filter({ $0.friend?.starred == true })[index].friend!.photo!.isEmpty {
|
||||
AsyncImage(
|
||||
url: URL(string: magicSearch.lastSearch.filter({ $0.friend?.starred == true })[index].friend!.photo!)
|
||||
) { image in
|
||||
switch image {
|
||||
case .empty:
|
||||
ProgressView()
|
||||
.frame(width: 45, height: 45)
|
||||
case .success(let image):
|
||||
image
|
||||
.resizable()
|
||||
.frame(width: 45, height: 45)
|
||||
.clipShape(Circle())
|
||||
case .failure:
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
.frame(width: 45, height: 45)
|
||||
.clipShape(Circle())
|
||||
@unknown default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
.frame(width: 45, height: 45)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
Text((magicSearch.lastSearch.filter({ $0.friend?.starred == true })[index].friend?.name)!)
|
||||
.default_text_style(styleSize: 16)
|
||||
.frame( maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
.simultaneousGesture(
|
||||
LongPressGesture()
|
||||
.onEnded { _ in
|
||||
contactViewModel.selectedFriend = magicSearch.lastSearch.filter({ $0.friend?.starred == true })[index].friend
|
||||
showingSheet.toggle()
|
||||
}
|
||||
)
|
||||
.highPriorityGesture(
|
||||
TapGesture()
|
||||
.onEnded { _ in
|
||||
withAnimation {
|
||||
contactViewModel.displayedFriend = (
|
||||
magicSearch.lastSearch.filter({ $0.friend?.starred == true })[index].friend
|
||||
)!
|
||||
}
|
||||
}
|
||||
)
|
||||
.frame(minWidth: 70, maxWidth: 70)
|
||||
ForEach(0..<magicSearch.lastSearch.count, id: \.self) { index in
|
||||
if magicSearch.lastSearch[index].friend != nil && magicSearch.lastSearch[index].friend!.starred == true {
|
||||
Button {
|
||||
} label: {
|
||||
VStack {
|
||||
if magicSearch.lastSearch[index].friend!.photo != nil
|
||||
&& !magicSearch.lastSearch[index].friend!.photo!.isEmpty {
|
||||
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: magicSearch.lastSearch[index].friend!.photo!)
|
||||
) { image in
|
||||
switch image {
|
||||
case .empty:
|
||||
ProgressView()
|
||||
.frame(width: 45, height: 45)
|
||||
case .success(let image):
|
||||
image
|
||||
.resizable()
|
||||
.frame(width: 45, height: 45)
|
||||
.clipShape(Circle())
|
||||
case .failure:
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
.frame(width: 45, height: 45)
|
||||
.clipShape(Circle())
|
||||
@unknown default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
.frame(width: 45, height: 45)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
Text((magicSearch.lastSearch[index].friend?.name)!)
|
||||
.default_text_style(styleSize: 16)
|
||||
.frame( maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
.simultaneousGesture(
|
||||
LongPressGesture()
|
||||
.onEnded { _ in
|
||||
contactViewModel.selectedFriend = magicSearch.lastSearch[index].friend
|
||||
showingSheet.toggle()
|
||||
}
|
||||
)
|
||||
.highPriorityGesture(
|
||||
TapGesture()
|
||||
.onEnded { _ in
|
||||
withAnimation {
|
||||
contactViewModel.indexDisplayedFriend = index
|
||||
}
|
||||
}
|
||||
)
|
||||
.frame(minWidth: 70, maxWidth: 70)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.all, 10)
|
||||
|
|
|
|||
|
|
@ -20,11 +20,13 @@
|
|||
import linphonesw
|
||||
|
||||
class ContactViewModel: ObservableObject {
|
||||
|
||||
@Published var displayedFriend: Friend?
|
||||
|
||||
@Published var indexDisplayedFriend: Int?
|
||||
|
||||
var stringToCopy: String = ""
|
||||
|
||||
var selectedFriend: Friend?
|
||||
var selectedFriendToDelete: Friend?
|
||||
|
||||
private var magicSearch = MagicSearchSingleton.shared
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
class EditContactViewModel: ObservableObject {
|
||||
|
||||
@Published var selectedEditFriend: Friend?
|
||||
|
||||
@Published var firstName: String = ""
|
||||
@Published var lastName: String = ""
|
||||
@Published var sipAddresses: [String] = []
|
||||
@Published var phoneNumbers: [String] = []
|
||||
@Published var company: String = ""
|
||||
@Published var jobTitle: String = ""
|
||||
@Published var removePopup: Bool = false
|
||||
|
||||
init() {
|
||||
resetValues()
|
||||
}
|
||||
|
||||
func resetValues() {
|
||||
firstName = (selectedEditFriend == nil ? "" : selectedEditFriend!.vcard?.givenName) ?? ""
|
||||
lastName = (selectedEditFriend == nil ? "" : selectedEditFriend!.vcard?.familyName) ?? ""
|
||||
sipAddresses = []
|
||||
phoneNumbers = []
|
||||
company = (selectedEditFriend == nil ? "" : selectedEditFriend!.organization) ?? ""
|
||||
jobTitle = (selectedEditFriend == nil ? "" : selectedEditFriend!.jobTitle) ?? ""
|
||||
|
||||
if selectedEditFriend != nil {
|
||||
selectedEditFriend?.addresses.forEach({ address in
|
||||
sipAddresses.append(String(address.asStringUriOnly().dropFirst(4)))
|
||||
})
|
||||
|
||||
selectedEditFriend?.phoneNumbers.forEach({ phoneNumber in
|
||||
phoneNumbers.append(phoneNumber)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
sipAddresses.append("")
|
||||
phoneNumbers.append("")
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ struct ContentView: View {
|
|||
var magicSearch = MagicSearchSingleton.shared
|
||||
|
||||
@ObservedObject var contactViewModel: ContactViewModel
|
||||
@ObservedObject var editContactViewModel: EditContactViewModel
|
||||
@ObservedObject var historyViewModel: HistoryViewModel
|
||||
@ObservedObject private var coreContext = CoreContext.shared
|
||||
|
||||
|
|
@ -36,8 +37,10 @@ struct ContentView: View {
|
|||
@State private var searchIsActive = false
|
||||
@State private var text = ""
|
||||
@FocusState private var focusedField: Bool
|
||||
@State var isMenuOpen: Bool = false
|
||||
@State var isMenuOpen = false
|
||||
@State var isShowDeletePopup = false
|
||||
@State var isShowEditContactFragment = false
|
||||
@State var isShowDismissPopup = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
|
|
@ -73,7 +76,7 @@ struct ContentView: View {
|
|||
|
||||
Button(action: {
|
||||
self.index = 1
|
||||
contactViewModel.displayedFriend = nil
|
||||
contactViewModel.indexDisplayedFriend = nil
|
||||
}, label: {
|
||||
VStack {
|
||||
Image("phone")
|
||||
|
|
@ -273,7 +276,13 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
if self.index == 0 {
|
||||
ContactsView(contactViewModel: contactViewModel, historyViewModel: historyViewModel, isShowDeletePopup: $isShowDeletePopup)
|
||||
ContactsView(
|
||||
contactViewModel: contactViewModel,
|
||||
historyViewModel: historyViewModel,
|
||||
editContactViewModel: editContactViewModel,
|
||||
isShowEditContactFragment: $isShowEditContactFragment,
|
||||
isShowDeletePopup: $isShowDeletePopup
|
||||
)
|
||||
} else if self.index == 1 {
|
||||
HistoryView()
|
||||
}
|
||||
|
|
@ -328,7 +337,7 @@ struct ContentView: View {
|
|||
|
||||
Button(action: {
|
||||
self.index = 1
|
||||
contactViewModel.displayedFriend = nil
|
||||
contactViewModel.indexDisplayedFriend = nil
|
||||
}, label: {
|
||||
VStack {
|
||||
Image("phone")
|
||||
|
|
@ -358,7 +367,7 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
|
||||
if contactViewModel.displayedFriend != nil || !historyViewModel.historyTitle.isEmpty {
|
||||
if contactViewModel.indexDisplayedFriend != nil || !historyViewModel.historyTitle.isEmpty {
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
.frame(maxWidth:
|
||||
|
|
@ -369,7 +378,7 @@ struct ContentView: View {
|
|||
: 0
|
||||
)
|
||||
if self.index == 0 {
|
||||
ContactFragment(contactViewModel: contactViewModel, isShowDeletePopup: $isShowDeletePopup)
|
||||
ContactFragment(contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, isShowDeletePopup: $isShowDeletePopup, isShowDismissPopup: $isShowDismissPopup)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.gray100)
|
||||
.ignoresSafeArea(.keyboard)
|
||||
|
|
@ -413,31 +422,42 @@ struct ContentView: View {
|
|||
.ignoresSafeArea(.all)
|
||||
.zIndex(2)
|
||||
|
||||
if isShowEditContactFragment {
|
||||
EditContactFragment(editContactViewModel: editContactViewModel, isShowEditContactFragment: $isShowEditContactFragment, isShowDismissPopup: $isShowDismissPopup)
|
||||
.zIndex(3)
|
||||
.transition(.move(edge: .bottom))
|
||||
.onAppear {
|
||||
contactViewModel.indexDisplayedFriend = nil
|
||||
}
|
||||
}
|
||||
|
||||
if isShowDeletePopup {
|
||||
PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDeletePopup,
|
||||
title: Text(
|
||||
contactViewModel.selectedFriend != nil
|
||||
contactViewModel.selectedFriend != nil
|
||||
? "Delete \(contactViewModel.selectedFriend!.name!)?"
|
||||
: (contactViewModel.displayedFriend != nil
|
||||
? "Delete \(contactViewModel.displayedFriend!.name!)?"
|
||||
: (contactViewModel.indexDisplayedFriend != nil
|
||||
? "Delete \(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.name!)?"
|
||||
: "Error Name")),
|
||||
content: Text("This contact will be deleted definitively."),
|
||||
titleFirstButton: Text("Cancel"),
|
||||
actionFirstButton: {self.isShowDeletePopup.toggle()},
|
||||
actionFirstButton: {
|
||||
self.isShowDeletePopup.toggle()},
|
||||
titleSecondButton: Text("Ok"),
|
||||
actionSecondButton: {
|
||||
if contactViewModel.selectedFriend != nil {
|
||||
contactViewModel.selectedFriend!.remove()
|
||||
if contactViewModel.displayedFriend != nil && contactViewModel.selectedFriend!.name == contactViewModel.displayedFriend!.name {
|
||||
if contactViewModel.selectedFriendToDelete != nil {
|
||||
if contactViewModel.indexDisplayedFriend != nil {
|
||||
withAnimation {
|
||||
contactViewModel.displayedFriend = nil
|
||||
contactViewModel.indexDisplayedFriend = nil
|
||||
}
|
||||
}
|
||||
} else if contactViewModel.displayedFriend != nil {
|
||||
contactViewModel.displayedFriend!.remove()
|
||||
contactViewModel.selectedFriendToDelete!.remove()
|
||||
} else if contactViewModel.indexDisplayedFriend != nil {
|
||||
let tmpIndex = contactViewModel.indexDisplayedFriend
|
||||
withAnimation {
|
||||
contactViewModel.displayedFriend = nil
|
||||
contactViewModel.indexDisplayedFriend = nil
|
||||
}
|
||||
magicSearch.lastSearch[tmpIndex!].friend!.remove()
|
||||
}
|
||||
magicSearch.searchForContacts(
|
||||
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
|
||||
|
|
@ -448,6 +468,39 @@ struct ContentView: View {
|
|||
.onTapGesture {
|
||||
self.isShowDeletePopup.toggle()
|
||||
}
|
||||
.onAppear {
|
||||
contactViewModel.selectedFriendToDelete = contactViewModel.selectedFriend
|
||||
}
|
||||
}
|
||||
|
||||
if isShowDismissPopup {
|
||||
PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDismissPopup,
|
||||
title: Text("Don’t save modifications?"),
|
||||
content: Text("All modifications will be canceled."),
|
||||
titleFirstButton: Text("Cancel"),
|
||||
actionFirstButton: {self.isShowDismissPopup.toggle()},
|
||||
titleSecondButton: Text("Ok"),
|
||||
actionSecondButton: {
|
||||
if editContactViewModel.selectedEditFriend == nil {
|
||||
self.isShowDismissPopup.toggle()
|
||||
editContactViewModel.removePopup = true
|
||||
editContactViewModel.resetValues()
|
||||
withAnimation {
|
||||
isShowEditContactFragment.toggle()
|
||||
}
|
||||
} else {
|
||||
self.isShowDismissPopup.toggle()
|
||||
editContactViewModel.resetValues()
|
||||
withAnimation {
|
||||
editContactViewModel.removePopup = true
|
||||
}
|
||||
}
|
||||
})
|
||||
.background(.black.opacity(0.65))
|
||||
.zIndex(3)
|
||||
.onTapGesture {
|
||||
self.isShowDismissPopup.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -462,7 +515,7 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
.onRotate { newOrientation in
|
||||
if (contactViewModel.displayedFriend != nil || !historyViewModel.historyTitle.isEmpty) && searchIsActive {
|
||||
if (contactViewModel.indexDisplayedFriend != nil || !historyViewModel.historyTitle.isEmpty) && searchIsActive {
|
||||
self.focusedField = false
|
||||
} else if searchIsActive {
|
||||
self.focusedField = true
|
||||
|
|
@ -479,5 +532,5 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
ContentView(contactViewModel: ContactViewModel(), historyViewModel: HistoryViewModel())
|
||||
ContentView(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), historyViewModel: HistoryViewModel())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,11 +47,9 @@ class SharedMainViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
if preferences.object(forKey: displayProfileModeKey) == nil {
|
||||
print("displayProfileModeKeydisplayProfileModeKey nil")
|
||||
preferences.set(displayProfileMode, forKey: displayProfileModeKey)
|
||||
} else {
|
||||
displayProfileMode = preferences.bool(forKey: displayProfileModeKey)
|
||||
print("displayProfileModeKeydisplayProfileModeKey \(displayProfileMode)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
85
Linphone/Utils/PhotoPicker.swift
Normal file
85
Linphone/Utils/PhotoPicker.swift
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 PhotosUI
|
||||
|
||||
struct PhotoPicker: UIViewControllerRepresentable {
|
||||
typealias UIViewControllerType = PHPickerViewController
|
||||
|
||||
let filter: PHPickerFilter
|
||||
var limit: Int = 0
|
||||
let onComplete: ([PHPickerResult]) -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||
|
||||
var configuration = PHPickerConfiguration()
|
||||
configuration.filter = filter
|
||||
configuration.selectionLimit = limit
|
||||
|
||||
let controller = PHPickerViewController(configuration: configuration)
|
||||
|
||||
controller.delegate = context.coordinator
|
||||
return controller
|
||||
}
|
||||
|
||||
static func convertToUIImageArray(fromResults results: [PHPickerResult], onComplete: @escaping ([UIImage]?, Error?) -> Void) {
|
||||
var images = [UIImage]()
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
for result in results {
|
||||
dispatchGroup.enter()
|
||||
let itemProvider = result.itemProvider
|
||||
if itemProvider.canLoadObject(ofClass: UIImage.self) {
|
||||
itemProvider.loadObject(ofClass: UIImage.self) { (imageOrNil, errorOrNil) in
|
||||
if let error = errorOrNil {
|
||||
onComplete(nil, error)
|
||||
}
|
||||
if let image = imageOrNil as? UIImage {
|
||||
images.append(image)
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
onComplete(images, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
return Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: PHPickerViewControllerDelegate {
|
||||
|
||||
private let parent: PhotoPicker
|
||||
|
||||
init(_ parent: PhotoPicker) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
picker.dismiss(animated: true)
|
||||
parent.onComplete(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue