210 lines
8.0 KiB
Swift
210 lines
8.0 KiB
Swift
import SwiftUI
|
|
import Bedrock
|
|
|
|
/// Represents a contact field that has been added
|
|
struct AddedContactField: Identifiable, Equatable {
|
|
let id: UUID
|
|
let fieldType: ContactFieldType
|
|
var value: String
|
|
var title: String
|
|
|
|
init(id: UUID = UUID(), fieldType: ContactFieldType, value: String = "", title: String = "") {
|
|
self.id = id
|
|
self.fieldType = fieldType
|
|
self.value = value
|
|
self.title = title
|
|
}
|
|
|
|
static func == (lhs: AddedContactField, rhs: AddedContactField) -> Bool {
|
|
lhs.id == rhs.id && lhs.value == rhs.value && lhs.title == rhs.title
|
|
}
|
|
|
|
/// Returns the display value for this field (formatted for addresses, raw for others)
|
|
var displayValue: String {
|
|
fieldType.formattedDisplayValue(value)
|
|
}
|
|
|
|
/// Returns a short display value suitable for single-line display in lists
|
|
var shortDisplayValue: String {
|
|
if fieldType.id == "address" {
|
|
// For addresses, show single-line format in the list
|
|
if let address = PostalAddress.decode(from: value), address.hasValue {
|
|
return address.singleLineString
|
|
}
|
|
}
|
|
return value
|
|
}
|
|
}
|
|
|
|
/// Displays a vertical list of added contact fields with tap to edit and drag to reorder
|
|
struct AddedContactFieldsView: View {
|
|
@Binding var fields: [AddedContactField]
|
|
var themeColor: Color = Color(red: 0.2, green: 0.2, blue: 0.2)
|
|
let onEdit: (AddedContactField) -> Void
|
|
|
|
@State private var draggingField: AddedContactField?
|
|
|
|
var body: some View {
|
|
if fields.isEmpty {
|
|
EmptyView()
|
|
} else {
|
|
VStack(spacing: 0) {
|
|
ForEach(fields) { field in
|
|
FieldRow(
|
|
field: field,
|
|
themeColor: themeColor,
|
|
onTap: { onEdit(field) },
|
|
onDelete: { deleteField(field) }
|
|
)
|
|
.draggable(field.id.uuidString) {
|
|
// Drag preview
|
|
FieldRowPreview(field: field, themeColor: themeColor)
|
|
}
|
|
.dropDestination(for: String.self) { items, _ in
|
|
guard let droppedId = items.first,
|
|
let droppedUUID = UUID(uuidString: droppedId),
|
|
let fromIndex = fields.firstIndex(where: { $0.id == droppedUUID }),
|
|
let toIndex = fields.firstIndex(where: { $0.id == field.id }),
|
|
fromIndex != toIndex else {
|
|
return false
|
|
}
|
|
withAnimation(.spring(duration: Design.Animation.quick)) {
|
|
let movedField = fields.remove(at: fromIndex)
|
|
fields.insert(movedField, at: toIndex)
|
|
}
|
|
return true
|
|
}
|
|
|
|
if field.id != fields.last?.id {
|
|
Divider()
|
|
.padding(.leading, Design.CardSize.avatarSize + Design.Spacing.large + Design.Spacing.medium)
|
|
}
|
|
}
|
|
}
|
|
.background(Color.AppBackground.elevated)
|
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
|
}
|
|
}
|
|
|
|
private func deleteField(_ field: AddedContactField) {
|
|
withAnimation {
|
|
fields.removeAll { $0.id == field.id }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Preview shown while dragging a field
|
|
private struct FieldRowPreview: View {
|
|
let field: AddedContactField
|
|
let themeColor: Color
|
|
|
|
var body: some View {
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
Circle()
|
|
.fill(themeColor)
|
|
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
|
|
.overlay(
|
|
field.fieldType.iconImage()
|
|
.typography(.title3)
|
|
.foregroundStyle(.white)
|
|
)
|
|
|
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
|
Text(field.value.isEmpty ? field.fieldType.displayName : field.shortDisplayValue)
|
|
.typography(.subheading)
|
|
.foregroundStyle(Color.Text.primary)
|
|
.lineLimit(1)
|
|
|
|
if !field.title.isEmpty {
|
|
Text(field.title)
|
|
.typography(.caption)
|
|
.foregroundStyle(Color.Text.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
}
|
|
.padding(Design.Spacing.medium)
|
|
.background(Color.AppBackground.elevated)
|
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
|
|
.shadow(radius: Design.Shadow.radiusMedium)
|
|
}
|
|
}
|
|
|
|
/// A display row for a contact field - tap to edit, hold to drag
|
|
private struct FieldRow: View {
|
|
let field: AddedContactField
|
|
let themeColor: Color
|
|
let onTap: () -> Void
|
|
let onDelete: () -> Void
|
|
|
|
var body: some View {
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
// Drag handle
|
|
Image(systemName: "line.3.horizontal")
|
|
.typography(.caption)
|
|
.foregroundStyle(Color.Text.tertiary)
|
|
.frame(width: Design.Spacing.large)
|
|
|
|
// Icon
|
|
Circle()
|
|
.fill(themeColor)
|
|
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
|
|
.overlay(
|
|
field.fieldType.iconImage()
|
|
.typography(.title3)
|
|
.foregroundStyle(.white)
|
|
)
|
|
|
|
// Content - tap to edit
|
|
Button(action: onTap) {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
|
Text(field.value.isEmpty ? field.fieldType.valuePlaceholder : field.shortDisplayValue)
|
|
.typography(.subheading)
|
|
.foregroundStyle(field.value.isEmpty ? Color.Text.secondary : Color.Text.primary)
|
|
.lineLimit(1)
|
|
|
|
Text(field.title.isEmpty ? field.fieldType.displayName : field.title)
|
|
.typography(.caption)
|
|
.foregroundStyle(Color.Text.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.buttonStyle(.plain)
|
|
|
|
// Delete button
|
|
Button(action: onDelete) {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.typography(.title3)
|
|
.foregroundStyle(Color.Text.secondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel(String(localized: "Delete"))
|
|
.accessibilityHint(String(localized: "Removes this field"))
|
|
}
|
|
.padding(Design.Spacing.medium)
|
|
.contentShape(.rect)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
@Previewable @State var fields: [AddedContactField] = {
|
|
let address = PostalAddress(street: "6565 Headquarters Dr", city: "Plano", state: "TX", postalCode: "75024")
|
|
return [
|
|
AddedContactField(fieldType: .email, value: "matt@example.com", title: "Work"),
|
|
AddedContactField(fieldType: .email, value: "personal@example.com", title: "Personal"),
|
|
AddedContactField(fieldType: .phone, value: "+1 (555) 123-4567", title: "Cell"),
|
|
AddedContactField(fieldType: .address, value: address.encode(), title: "Work"),
|
|
AddedContactField(fieldType: .linkedIn, value: "linkedin.com/in/mattbruce", title: "Connect with me")
|
|
]
|
|
}()
|
|
|
|
ScrollView {
|
|
AddedContactFieldsView(fields: $fields) { field in
|
|
Design.debugLog("Edit: \(field.fieldType.displayName)")
|
|
}
|
|
.padding()
|
|
}
|
|
.background(Color.AppBackground.base)
|
|
}
|