Support.swift
184 lignes · 6348 octets
import SwiftUI import UIKit // Shared UI helpers: display names, top bars, path-based images, external // links and draft image persistence. // Kotlin `contactDisplayName` (CarnetScreen/ContactPagerScreen). func contactDisplayName(_ c: CrmContactEntity) -> String { if let full = c.fullName?.nilIfBlank { return full } let composed = [c.firstName, c.lastName].compactMap { $0 } .joined(separator: " ") .trimmingCharacters(in: .whitespaces) if !composed.isEmpty { return composed } if let company = c.company?.nilIfBlank { return company } return "—" } // Kotlin `contactSubtitle` (CarnetScreen). func contactSubtitle(_ c: CrmContactEntity) -> String? { let name: String? if let full = c.fullName?.nilIfBlank { name = full } else { let composed = [c.firstName, c.lastName].compactMap { $0 } .joined(separator: " ") .trimmingCharacters(in: .whitespaces) name = composed.isEmpty ? nil : composed } if name != nil { return c.company?.nilIfBlank } return c.jobTitle?.nilIfBlank } /// Custom top bar mirroring the Android screens' own header rows /// (system navigation bars are hidden on ported screens). struct ScreenTopBar<Trailing: View>: View { let title: String var centered: Bool = true let onBack: () -> Void @ViewBuilder var trailing: () -> Trailing init( title: String, centered: Bool = true, onBack: @escaping () -> Void, @ViewBuilder trailing: @escaping () -> Trailing = { EmptyView() } ) { self.title = title self.centered = centered self.onBack = onBack self.trailing = trailing } var body: some View { ZStack { HStack(spacing: 4) { Button(action: onBack) { Image(systemName: "arrow.left") .foregroundColor(C2VColor.ink) .frame(width: 44, height: 44) } .accessibilityLabel("Retour") if !centered { Text(title) .font(C2VFont.titleMedium) .foregroundColor(C2VColor.encre) } Spacer() trailing() } if centered { Text(title) .font(C2VFont.titleMedium) .foregroundColor(C2VColor.encre) } } .padding(.horizontal, 4) .padding(.vertical, 4) .background(C2VColor.surface) } } /// Kotlin `PathImage` — renders nothing when the file is missing. struct PathImage: View { let path: String? var contentMode: ContentMode = .fill /// Bumps identity when the underlying file changed (Kotlin `revision`). var revision: Int64 = 0 var body: some View { if let path = path?.nilIfBlank, let ui = UIImage(contentsOfFile: path) { Image(uiImage: ui) .resizable() .aspectRatio(contentMode: contentMode) .id("\(path)#\(revision)") } } } /// Android `ACTION_DIAL` / `ACTION_SENDTO` equivalents. enum ExternalLinks { static func dial(_ phone: String) { let tel = phone.filter { $0.isNumber || $0 == "+" } guard !tel.isEmpty, let url = URL(string: "tel:\(tel)") else { return } UIApplication.shared.open(url) } static func email(_ address: String) { let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? address guard let url = URL(string: "mailto:\(encoded)") else { return } UIApplication.shared.open(url) } static func web(_ site: String) { var raw = site.trimmingCharacters(in: .whitespaces) if !raw.lowercased().hasPrefix("http://") && !raw.lowercased().hasPrefix("https://") { raw = "https://\(raw)" } guard let url = URL(string: raw) else { return } UIApplication.shared.open(url) } } /// Persists UI-captured images as JPEG files; the repository stores paths only /// (Android saved Bitmaps through `ContactImageStore`). enum DraftImageWriter { static func writeJpeg(_ image: UIImage, prefix: String) -> String? { guard let data = image.jpegData(compressionQuality: 0.9) else { return nil } let fm = FileManager.default guard let base = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { return nil } let dir = base.appendingPathComponent("images", isDirectory: true) do { try fm.createDirectory(at: dir, withIntermediateDirectories: true) let url = dir.appendingPathComponent("\(prefix)-\(UUID().uuidString).jpg") try data.write(to: url, options: .atomic) return url.path } catch { return nil } } } /// `UIImagePickerController` camera wrapper for the profile-photo capture /// (Android `ActivityResultContracts.TakePicture`). struct CameraPickerView: UIViewControllerRepresentable { let completion: (UIImage?) -> Void static var isAvailable: Bool { UIImagePickerController.isSourceTypeAvailable(.camera) } func makeUIViewController(context: Context) -> UIImagePickerController { let controller = UIImagePickerController() controller.sourceType = .camera controller.delegate = context.coordinator return controller } func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(completion: completion) } final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { private let completion: (UIImage?) -> Void init(completion: @escaping (UIImage?) -> Void) { self.completion = completion } func imagePickerController( _ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] ) { completion(info[.originalImage] as? UIImage) } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { completion(nil) } } }
GitRust