Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
a164669cf2
commit
fadb46e9cd
@ -532,7 +532,7 @@
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = BusinessCard/Info.plist;
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "for testing purposes";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "BusinessCard uses your photo library to add a profile photo to your business card.";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@ -568,7 +568,7 @@
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = BusinessCard/Info.plist;
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "for testing purposes";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "BusinessCard uses your photo library to add a profile photo to your business card.";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
|
||||
@ -7,12 +7,12 @@
|
||||
<key>BusinessCard.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>BusinessCardWatch.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@ -98,6 +98,7 @@ struct BusinessCardApp: App {
|
||||
WindowGroup {
|
||||
RootTabView()
|
||||
.environment(appState)
|
||||
.preferredColorScheme(.light)
|
||||
}
|
||||
.modelContainer(modelContainer)
|
||||
}
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>BusinessCard uses the camera to scan QR codes on other people's business cards.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>BusinessCard uses your photo library to add a profile photo to your business card.</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
|
||||
@ -21,7 +21,19 @@ enum CardTheme: String, CaseIterable, Identifiable, Hashable, Sendable {
|
||||
|
||||
static var all: [CardTheme] { allCases }
|
||||
|
||||
// RGB values - nonisolated
|
||||
// MARK: - Theme Brightness
|
||||
|
||||
/// Whether this theme requires dark text for proper contrast.
|
||||
/// Light backgrounds need dark text; dark backgrounds need light text.
|
||||
var requiresDarkText: Bool {
|
||||
switch self {
|
||||
case .lime: return true
|
||||
case .coral, .midnight, .ocean, .violet: return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RGB Values (nonisolated)
|
||||
|
||||
private var primaryRGB: (Double, Double, Double) {
|
||||
switch self {
|
||||
case .coral: return (0.95, 0.35, 0.33)
|
||||
@ -52,7 +64,14 @@ enum CardTheme: String, CaseIterable, Identifiable, Hashable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
// Colors - computed from RGB
|
||||
private var textRGB: (Double, Double, Double) {
|
||||
requiresDarkText
|
||||
? (0.14, 0.14, 0.17) // Dark text for light backgrounds
|
||||
: (0.98, 0.98, 0.98) // Light text for dark backgrounds
|
||||
}
|
||||
|
||||
// MARK: - Colors (MainActor)
|
||||
|
||||
@MainActor var primaryColor: Color {
|
||||
Color(red: primaryRGB.0, green: primaryRGB.1, blue: primaryRGB.2)
|
||||
}
|
||||
@ -64,4 +83,9 @@ enum CardTheme: String, CaseIterable, Identifiable, Hashable, Sendable {
|
||||
@MainActor var accentColor: Color {
|
||||
Color(red: accentRGB.0, green: accentRGB.1, blue: accentRGB.2)
|
||||
}
|
||||
|
||||
/// The appropriate text color for content displayed on this theme's background.
|
||||
@MainActor var textColor: Color {
|
||||
Color(red: textRGB.0, green: textRGB.1, blue: textRGB.2)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1013,9 +1013,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Record who received your card" : {
|
||||
|
||||
},
|
||||
"Reviews" : {
|
||||
"extractionState" : "stale",
|
||||
@ -1553,9 +1550,6 @@
|
||||
},
|
||||
"This person will appear in your Contacts tab so you can track who has your card." : {
|
||||
|
||||
},
|
||||
"Track this share" : {
|
||||
|
||||
},
|
||||
"Track who receives your card" : {
|
||||
"extractionState" : "stale",
|
||||
|
||||
@ -47,7 +47,7 @@ private struct StackedCardLayout: View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
CardHeaderView(card: card)
|
||||
Divider()
|
||||
.overlay(Color.Text.inverted.opacity(Design.Opacity.medium))
|
||||
.overlay(card.theme.textColor.opacity(Design.Opacity.medium))
|
||||
CardDetailsView(card: card)
|
||||
if card.hasSocialLinks {
|
||||
SocialLinksRow(card: card)
|
||||
@ -69,7 +69,7 @@ private struct SplitCardLayout: View {
|
||||
}
|
||||
}
|
||||
Spacer(minLength: Design.Spacing.medium)
|
||||
AccentBlockView(color: card.theme.accentColor)
|
||||
AccentBlockView(color: card.theme.accentColor, textColor: card.theme.textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -90,7 +90,8 @@ private struct PhotoCardLayout: View {
|
||||
AvatarBadgeView(
|
||||
systemName: card.avatarSystemName,
|
||||
accentColor: card.theme.accentColor,
|
||||
photoData: card.photoData
|
||||
photoData: card.photoData,
|
||||
borderColor: card.theme.textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -100,58 +101,63 @@ private struct PhotoCardLayout: View {
|
||||
|
||||
private struct CardHeaderView: View {
|
||||
let card: BusinessCard
|
||||
|
||||
private var textColor: Color { card.theme.textColor }
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
AvatarBadgeView(
|
||||
systemName: card.avatarSystemName,
|
||||
accentColor: card.theme.accentColor,
|
||||
photoData: card.photoData
|
||||
photoData: card.photoData,
|
||||
borderColor: textColor
|
||||
)
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
Text(card.displayName)
|
||||
.font(.headline)
|
||||
.bold()
|
||||
.foregroundStyle(Color.Text.inverted)
|
||||
.foregroundStyle(textColor)
|
||||
|
||||
if !card.pronouns.isEmpty {
|
||||
Text("(\(card.pronouns))")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.strong))
|
||||
.foregroundStyle(textColor.opacity(Design.Opacity.strong))
|
||||
}
|
||||
}
|
||||
Text(card.role)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.almostFull))
|
||||
.foregroundStyle(textColor.opacity(Design.Opacity.almostFull))
|
||||
Text(card.company)
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.medium))
|
||||
.foregroundStyle(textColor.opacity(Design.Opacity.medium))
|
||||
}
|
||||
Spacer(minLength: Design.Spacing.small)
|
||||
LabelBadgeView(label: card.label, accentColor: card.theme.accentColor)
|
||||
LabelBadgeView(label: card.label, accentColor: card.theme.accentColor, textColor: textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CardDetailsView: View {
|
||||
let card: BusinessCard
|
||||
|
||||
private var textColor: Color { card.theme.textColor }
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||
if !card.email.isEmpty {
|
||||
IconRowView(systemImage: "envelope", text: card.email)
|
||||
IconRowView(systemImage: "envelope", text: card.email, textColor: textColor)
|
||||
}
|
||||
if !card.phone.isEmpty {
|
||||
IconRowView(systemImage: "phone", text: card.phone)
|
||||
IconRowView(systemImage: "phone", text: card.phone, textColor: textColor)
|
||||
}
|
||||
if !card.website.isEmpty {
|
||||
IconRowView(systemImage: "link", text: card.website)
|
||||
IconRowView(systemImage: "link", text: card.website, textColor: textColor)
|
||||
}
|
||||
if !card.bio.isEmpty {
|
||||
Text(card.bio)
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.strong))
|
||||
.foregroundStyle(textColor.opacity(Design.Opacity.strong))
|
||||
.lineLimit(2)
|
||||
.padding(.top, Design.Spacing.xxSmall)
|
||||
}
|
||||
@ -162,25 +168,27 @@ private struct CardDetailsView: View {
|
||||
private struct SocialLinksRow: View {
|
||||
let card: BusinessCard
|
||||
|
||||
private var textColor: Color { card.theme.textColor }
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
if !card.linkedIn.isEmpty {
|
||||
SocialIconView(systemImage: "link")
|
||||
SocialIconView(systemImage: "link", textColor: textColor)
|
||||
}
|
||||
if !card.twitter.isEmpty {
|
||||
SocialIconView(systemImage: "at")
|
||||
SocialIconView(systemImage: "at", textColor: textColor)
|
||||
}
|
||||
if !card.instagram.isEmpty {
|
||||
SocialIconView(systemImage: "camera")
|
||||
SocialIconView(systemImage: "camera", textColor: textColor)
|
||||
}
|
||||
if !card.facebook.isEmpty {
|
||||
SocialIconView(systemImage: "person.2")
|
||||
SocialIconView(systemImage: "person.2", textColor: textColor)
|
||||
}
|
||||
if !card.tiktok.isEmpty {
|
||||
SocialIconView(systemImage: "play.rectangle")
|
||||
SocialIconView(systemImage: "play.rectangle", textColor: textColor)
|
||||
}
|
||||
if !card.github.isEmpty {
|
||||
SocialIconView(systemImage: "chevron.left.forwardslash.chevron.right")
|
||||
SocialIconView(systemImage: "chevron.left.forwardslash.chevron.right", textColor: textColor)
|
||||
}
|
||||
}
|
||||
.padding(.top, Design.Spacing.xxSmall)
|
||||
@ -191,19 +199,21 @@ private struct SocialLinksRow: View {
|
||||
|
||||
private struct SocialIconView: View {
|
||||
let systemImage: String
|
||||
var textColor: Color = Color.Text.inverted
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: systemImage)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.strong))
|
||||
.foregroundStyle(textColor.opacity(Design.Opacity.strong))
|
||||
.frame(width: Design.Spacing.xLarge, height: Design.Spacing.xLarge)
|
||||
.background(Color.Text.inverted.opacity(Design.Opacity.hint))
|
||||
.background(textColor.opacity(Design.Opacity.hint))
|
||||
.clipShape(.circle)
|
||||
}
|
||||
}
|
||||
|
||||
private struct AccentBlockView: View {
|
||||
let color: Color
|
||||
var textColor: Color = Color.Text.inverted
|
||||
|
||||
var body: some View {
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
||||
@ -211,7 +221,7 @@ private struct AccentBlockView: View {
|
||||
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
|
||||
.overlay(
|
||||
Image(systemName: "bolt.fill")
|
||||
.foregroundStyle(Color.Text.inverted)
|
||||
.foregroundStyle(textColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -294,19 +294,23 @@ private struct PhotoPickerRow: View {
|
||||
let avatarSystemName: String
|
||||
|
||||
var body: some View {
|
||||
let hasPhoto = photoData != nil
|
||||
let labelText = hasPhoto ? String.localized("Change Photo") : String.localized("Add Photo")
|
||||
let accentColor = Color.Accent.red
|
||||
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
AvatarBadgeView(
|
||||
systemName: avatarSystemName,
|
||||
accentColor: Color.Accent.red,
|
||||
accentColor: accentColor,
|
||||
photoData: photoData
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||
PhotosPicker(selection: $selectedPhoto, matching: .images) {
|
||||
Text(photoData == nil ? String.localized("Add Photo") : String.localized("Change Photo"))
|
||||
.foregroundStyle(Color.Accent.red)
|
||||
Text(labelText)
|
||||
.foregroundStyle(accentColor)
|
||||
}
|
||||
if photoData != nil {
|
||||
if hasPhoto {
|
||||
Button(String.localized("Remove Photo"), role: .destructive) {
|
||||
photoData = nil
|
||||
selectedPhoto = nil
|
||||
@ -349,21 +353,24 @@ private struct EditorCardPreview: View {
|
||||
let theme: CardTheme
|
||||
let photoData: Data?
|
||||
|
||||
private var textColor: Color { theme.textColor }
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
AvatarBadgeView(
|
||||
systemName: avatarSystemName,
|
||||
accentColor: theme.accentColor,
|
||||
photoData: photoData
|
||||
photoData: photoData,
|
||||
borderColor: textColor
|
||||
)
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||
Text(displayName).font(.headline).bold().foregroundStyle(Color.Text.inverted)
|
||||
Text(role).font(.subheadline).foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.almostFull))
|
||||
Text(company).font(.caption).foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.medium))
|
||||
Text(displayName).font(.headline).bold().foregroundStyle(textColor)
|
||||
Text(role).font(.subheadline).foregroundStyle(textColor.opacity(Design.Opacity.almostFull))
|
||||
Text(company).font(.caption).foregroundStyle(textColor.opacity(Design.Opacity.medium))
|
||||
}
|
||||
Spacer(minLength: Design.Spacing.small)
|
||||
LabelBadgeView(label: label, accentColor: theme.accentColor)
|
||||
LabelBadgeView(label: label, accentColor: theme.accentColor, textColor: textColor)
|
||||
}
|
||||
}
|
||||
.padding(Design.Spacing.large)
|
||||
|
||||
@ -8,17 +8,20 @@ struct AvatarBadgeView: View {
|
||||
let accentColor: Color
|
||||
let photoData: Data?
|
||||
let size: CGFloat
|
||||
let borderColor: Color
|
||||
|
||||
init(
|
||||
systemName: String = "person.crop.circle",
|
||||
accentColor: Color = Color.Accent.red,
|
||||
photoData: Data? = nil,
|
||||
size: CGFloat = Design.CardSize.avatarSize
|
||||
size: CGFloat = Design.CardSize.avatarSize,
|
||||
borderColor: Color = Color.Text.inverted
|
||||
) {
|
||||
self.systemName = systemName
|
||||
self.accentColor = accentColor
|
||||
self.photoData = photoData
|
||||
self.size = size
|
||||
self.borderColor = borderColor
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -30,11 +33,11 @@ struct AvatarBadgeView: View {
|
||||
.clipShape(.circle)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.Text.inverted.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
|
||||
.stroke(borderColor.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
|
||||
)
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color.Text.inverted)
|
||||
.fill(borderColor)
|
||||
.frame(width: size, height: size)
|
||||
.overlay(
|
||||
Image(systemName: systemName)
|
||||
@ -42,7 +45,7 @@ struct AvatarBadgeView: View {
|
||||
)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.Text.inverted.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
|
||||
.stroke(borderColor.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -58,7 +61,8 @@ struct AvatarBadgeView: View {
|
||||
AvatarBadgeView(
|
||||
systemName: "briefcase.fill",
|
||||
accentColor: Color.CardPalette.ocean,
|
||||
size: 80
|
||||
size: 80,
|
||||
borderColor: Color.Text.primary
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
|
||||
@ -2,11 +2,11 @@ import SwiftUI
|
||||
import Bedrock
|
||||
|
||||
/// A row with an icon and text, used for contact info display.
|
||||
/// Supports both inverted (on dark) and standard (on light) styles.
|
||||
/// Accepts a text color for flexibility with different background themes.
|
||||
struct IconRowView: View {
|
||||
let systemImage: String
|
||||
let text: String
|
||||
var inverted: Bool = true
|
||||
var textColor: Color = Color.Text.inverted
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
@ -19,17 +19,13 @@ struct IconRowView: View {
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
private var textColor: Color {
|
||||
inverted ? Color.Text.inverted : Color.Text.primary
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
IconRowView(systemImage: "envelope", text: "hello@example.com")
|
||||
IconRowView(systemImage: "phone", text: "+1 555 123 4567")
|
||||
IconRowView(systemImage: "link", text: "example.com", inverted: false)
|
||||
IconRowView(systemImage: "link", text: "example.com", textColor: Color.Text.primary)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.CardPalette.midnight)
|
||||
|
||||
@ -5,12 +5,13 @@ import Bedrock
|
||||
struct LabelBadgeView: View {
|
||||
let label: String
|
||||
let accentColor: Color
|
||||
var textColor: Color = Color.Text.inverted
|
||||
|
||||
var body: some View {
|
||||
Text(String.localized(label))
|
||||
.font(.caption)
|
||||
.bold()
|
||||
.foregroundStyle(Color.Text.inverted)
|
||||
.foregroundStyle(textColor)
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
.padding(.vertical, Design.Spacing.xxSmall)
|
||||
.background(accentColor.opacity(Design.Opacity.medium))
|
||||
@ -21,7 +22,7 @@ struct LabelBadgeView: View {
|
||||
#Preview {
|
||||
HStack {
|
||||
LabelBadgeView(label: "Work", accentColor: Color.CardPalette.coral)
|
||||
LabelBadgeView(label: "Personal", accentColor: Color.CardPalette.ocean)
|
||||
LabelBadgeView(label: "Personal", accentColor: Color.CardPalette.ocean, textColor: Color.Text.primary)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.CardPalette.midnight)
|
||||
|
||||
@ -81,6 +81,8 @@ struct ShareCardView: View {
|
||||
|
||||
private struct QRCodeCardView: View {
|
||||
let card: BusinessCard
|
||||
|
||||
private var textColor: Color { card.theme.textColor }
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
@ -92,7 +94,7 @@ private struct QRCodeCardView: View {
|
||||
|
||||
Text("Point your camera at the QR code to receive the card")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Color.Text.secondary)
|
||||
.foregroundStyle(textColor.opacity(Design.Opacity.strong))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(Design.Spacing.large)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user