Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
8c40e638e5
commit
954f21616e
@ -51,6 +51,14 @@ final class ContactField {
|
|||||||
fieldType?.displayName ?? typeId
|
fieldType?.displayName ?? typeId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formatted display value using the field type's formatter
|
||||||
|
/// For addresses, this returns a multi-line formatted string
|
||||||
|
/// For other fields, returns the value as-is
|
||||||
|
@MainActor
|
||||||
|
var displayValue: String {
|
||||||
|
fieldType?.formattedDisplayValue(value) ?? value
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns an Image view for this field's icon
|
/// Returns an Image view for this field's icon
|
||||||
@MainActor
|
@MainActor
|
||||||
func iconImage() -> Image {
|
func iconImage() -> Image {
|
||||||
|
|||||||
@ -39,6 +39,7 @@ struct ContactFieldType: Identifiable, Hashable, Sendable {
|
|||||||
let keyboardType: UIKeyboardType
|
let keyboardType: UIKeyboardType
|
||||||
let autocapitalization: TextInputAutocapitalization
|
let autocapitalization: TextInputAutocapitalization
|
||||||
let urlBuilder: @Sendable (String) -> URL?
|
let urlBuilder: @Sendable (String) -> URL?
|
||||||
|
let displayValueFormatter: @Sendable (String) -> String
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: String,
|
id: String,
|
||||||
@ -52,7 +53,8 @@ struct ContactFieldType: Identifiable, Hashable, Sendable {
|
|||||||
titleSuggestions: [String],
|
titleSuggestions: [String],
|
||||||
keyboardType: UIKeyboardType,
|
keyboardType: UIKeyboardType,
|
||||||
autocapitalization: TextInputAutocapitalization = .never,
|
autocapitalization: TextInputAutocapitalization = .never,
|
||||||
urlBuilder: @escaping @Sendable (String) -> URL?
|
urlBuilder: @escaping @Sendable (String) -> URL?,
|
||||||
|
displayValueFormatter: @escaping @Sendable (String) -> String = { $0 }
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.displayName = displayName
|
self.displayName = displayName
|
||||||
@ -66,6 +68,7 @@ struct ContactFieldType: Identifiable, Hashable, Sendable {
|
|||||||
self.keyboardType = keyboardType
|
self.keyboardType = keyboardType
|
||||||
self.autocapitalization = autocapitalization
|
self.autocapitalization = autocapitalization
|
||||||
self.urlBuilder = urlBuilder
|
self.urlBuilder = urlBuilder
|
||||||
|
self.displayValueFormatter = displayValueFormatter
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an Image view for this field type's icon
|
/// Returns an Image view for this field type's icon
|
||||||
@ -93,6 +96,13 @@ struct ContactFieldType: Identifiable, Hashable, Sendable {
|
|||||||
func buildURL(value: String) -> URL? {
|
func buildURL(value: String) -> URL? {
|
||||||
urlBuilder(value)
|
urlBuilder(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Display Value Formatting
|
||||||
|
|
||||||
|
/// Returns a formatted display value for the given raw value
|
||||||
|
func formattedDisplayValue(_ value: String) -> String {
|
||||||
|
displayValueFormatter(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - All Field Types
|
// MARK: - All Field Types
|
||||||
@ -183,7 +193,8 @@ extension ContactFieldType {
|
|||||||
urlBuilder: { value in
|
urlBuilder: { value in
|
||||||
let encoded = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value
|
let encoded = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value
|
||||||
return URL(string: "maps://?q=\(encoded)")
|
return URL(string: "maps://?q=\(encoded)")
|
||||||
}
|
},
|
||||||
|
displayValueFormatter: formatAddressForDisplay
|
||||||
)
|
)
|
||||||
|
|
||||||
// MARK: - Social Media
|
// MARK: - Social Media
|
||||||
@ -641,6 +652,26 @@ extension ContactFieldType {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Display Value Formatters
|
||||||
|
|
||||||
|
/// Formats an address for multi-line display
|
||||||
|
/// Splits on commas and puts each component on a new line
|
||||||
|
nonisolated private func formatAddressForDisplay(_ value: String) -> String {
|
||||||
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmed.isEmpty else { return value }
|
||||||
|
|
||||||
|
// Split by comma, trim each component, and join with newlines
|
||||||
|
let components = trimmed
|
||||||
|
.split(separator: ",")
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
|
||||||
|
// If only one component, return as-is
|
||||||
|
guard components.count > 1 else { return trimmed }
|
||||||
|
|
||||||
|
return components.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - URL Helper Functions
|
// MARK: - URL Helper Functions
|
||||||
|
|
||||||
nonisolated private func buildWebURL(_ value: String) -> URL? {
|
nonisolated private func buildWebURL(_ value: String) -> URL? {
|
||||||
|
|||||||
@ -606,6 +606,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"The default card is used for sharing and widgets." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"This doesn't appear to be a business card QR code." : {
|
"This doesn't appear to be a business card QR code." : {
|
||||||
|
|
||||||
|
|||||||
@ -44,6 +44,9 @@ struct CardEditorView: View {
|
|||||||
@State private var coverPhotoData: Data?
|
@State private var coverPhotoData: Data?
|
||||||
@State private var logoData: Data?
|
@State private var logoData: Data?
|
||||||
|
|
||||||
|
// Default card
|
||||||
|
@State private var isDefault = false
|
||||||
|
|
||||||
// Photo editor state - just one variable!
|
// Photo editor state - just one variable!
|
||||||
@State private var editingImageType: ImageType?
|
@State private var editingImageType: ImageType?
|
||||||
|
|
||||||
@ -106,6 +109,15 @@ struct CardEditorView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
|
// Default card toggle
|
||||||
|
Section {
|
||||||
|
Toggle(isOn: $isDefault) {
|
||||||
|
Label(String.localized("Default Card"), systemImage: "checkmark.seal.fill")
|
||||||
|
}
|
||||||
|
} footer: {
|
||||||
|
Text("The default card is used for sharing and widgets.")
|
||||||
|
}
|
||||||
|
|
||||||
// Card Style section
|
// Card Style section
|
||||||
Section {
|
Section {
|
||||||
CardStylePicker(selectedTheme: $selectedTheme)
|
CardStylePicker(selectedTheme: $selectedTheme)
|
||||||
@ -651,6 +663,7 @@ private extension CardEditorView {
|
|||||||
bio = card.bio
|
bio = card.bio
|
||||||
accreditations = card.accreditations
|
accreditations = card.accreditations
|
||||||
avatarSystemName = card.avatarSystemName
|
avatarSystemName = card.avatarSystemName
|
||||||
|
isDefault = card.isDefault
|
||||||
|
|
||||||
// Load contact fields from the array
|
// Load contact fields from the array
|
||||||
contactFields = card.orderedContactFields.compactMap { $0.toAddedContactField() }
|
contactFields = card.orderedContactFields.compactMap { $0.toAddedContactField() }
|
||||||
@ -693,9 +706,19 @@ private extension CardEditorView {
|
|||||||
if let existingCard = card {
|
if let existingCard = card {
|
||||||
updateCard(existingCard)
|
updateCard(existingCard)
|
||||||
onSave(existingCard)
|
onSave(existingCard)
|
||||||
|
|
||||||
|
// Handle default card toggle
|
||||||
|
if isDefault {
|
||||||
|
appState.cardStore.setDefaultCard(existingCard)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let newCard = createCard()
|
let newCard = createCard()
|
||||||
onSave(newCard)
|
onSave(newCard)
|
||||||
|
|
||||||
|
// Handle default card toggle for new cards
|
||||||
|
if isDefault {
|
||||||
|
appState.cardStore.setDefaultCard(newCard)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,12 +27,8 @@ struct CardsHomeView: View {
|
|||||||
// Full-screen swipeable cards
|
// Full-screen swipeable cards
|
||||||
TabView(selection: $cardStore.selectedCardID) {
|
TabView(selection: $cardStore.selectedCardID) {
|
||||||
ForEach(cardStore.cards) { card in
|
ForEach(cardStore.cards) { card in
|
||||||
CardPageView(
|
CardPageView(card: card)
|
||||||
card: card,
|
.tag(Optional(card.id))
|
||||||
isDefault: card.isDefault,
|
|
||||||
onSetDefault: { cardStore.setDefaultCard(card) }
|
|
||||||
)
|
|
||||||
.tag(Optional(card.id))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tabViewStyle(.page(indexDisplayMode: .automatic))
|
.tabViewStyle(.page(indexDisplayMode: .automatic))
|
||||||
@ -97,23 +93,11 @@ struct CardsHomeView: View {
|
|||||||
|
|
||||||
private struct CardPageView: View {
|
private struct CardPageView: View {
|
||||||
let card: BusinessCard
|
let card: BusinessCard
|
||||||
let isDefault: Bool
|
|
||||||
let onSetDefault: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: Design.Spacing.large) {
|
VStack(spacing: Design.Spacing.large) {
|
||||||
BusinessCardView(card: card)
|
BusinessCardView(card: card)
|
||||||
|
|
||||||
// Default card toggle
|
|
||||||
Button(
|
|
||||||
isDefault ? String.localized("Default card") : String.localized("Set as default"),
|
|
||||||
systemImage: isDefault ? "checkmark.seal.fill" : "checkmark.seal",
|
|
||||||
action: onSetDefault
|
|
||||||
)
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.tint(Color.Accent.red)
|
|
||||||
.accessibilityHint(String.localized("Sets this card as your default sharing card"))
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Design.Spacing.large)
|
.padding(.horizontal, Design.Spacing.large)
|
||||||
.padding(.vertical, Design.Spacing.xLarge)
|
.padding(.vertical, Design.Spacing.xLarge)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user