feat: settings screen + iOS settings views
This commit is contained in:
parent
b90b9bca77
commit
42706b4dc6
|
|
@ -36,8 +36,8 @@ import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -117,30 +117,40 @@ fun SettingsSubpage(
|
||||||
fun SettingsSection(
|
fun SettingsSection(
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
accent: Color = Color(0xFFF7C8E4),
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Card(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(),
|
||||||
.background(SettingsCard, RoundedCornerShape(16.dp))
|
shape = RoundedCornerShape(22.dp),
|
||||||
.padding(vertical = 8.dp),
|
colors = CardDefaults.cardColors(containerColor = accent.copy(alpha = 0.16f)),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 5.dp)
|
||||||
) {
|
) {
|
||||||
title?.let {
|
Column(
|
||||||
Text(
|
modifier = Modifier
|
||||||
text = it,
|
.fillMaxWidth()
|
||||||
style = MaterialTheme.typography.labelMedium,
|
.padding(1.dp)
|
||||||
color = SettingsMuted,
|
.background(SettingsCard.copy(alpha = 0.92f), RoundedCornerShape(21.dp))
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(vertical = 8.dp),
|
||||||
)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
title?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = SettingsMuted,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
content()
|
||||||
}
|
}
|
||||||
content()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsSectionDivider() {
|
fun SettingsSectionDivider() {
|
||||||
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
|
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================
|
// ====================
|
||||||
|
|
@ -287,18 +297,28 @@ fun SettingsScreen(
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||||
) {
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Manage your space, reminders, privacy, and account.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = SettingsMuted,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
// Profile card — editable identity (currently local-only)
|
// Profile card — editable identity (currently local-only)
|
||||||
Card(
|
Card(
|
||||||
onClick = { onNavigate(AppRoute.ACCOUNT) },
|
onClick = { onNavigate(AppRoute.ACCOUNT) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(22.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = SettingsCard)
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFFFF8FC).copy(alpha = 0.96f)),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 5.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(18.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
|
|
@ -345,13 +365,14 @@ fun SettingsScreen(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(22.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = SettingsCard)
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFF8F0FF).copy(alpha = 0.96f)),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 5.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(18.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
|
|
@ -399,9 +420,7 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(4.dp))
|
SettingsSection(title = "For the two of you", accent = Color(0xFFF7C8E4)) {
|
||||||
|
|
||||||
SettingsSection(title = "For the two of you") {
|
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
icon = Icons.Filled.Done,
|
icon = Icons.Filled.Done,
|
||||||
label = "Answer History",
|
label = "Answer History",
|
||||||
|
|
@ -417,7 +436,7 @@ fun SettingsScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSection(title = "Your rhythm") {
|
SettingsSection(title = "Your rhythm", accent = Color(0xFFD9B8FF)) {
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
icon = Icons.Filled.Notifications,
|
icon = Icons.Filled.Notifications,
|
||||||
label = "Notifications",
|
label = "Notifications",
|
||||||
|
|
@ -433,7 +452,7 @@ fun SettingsScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSection(title = "Premium") {
|
SettingsSection(title = "Premium", accent = Color(0xFFE7A2D1)) {
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
icon = Icons.Filled.Favorite,
|
icon = Icons.Filled.Favorite,
|
||||||
label = "Subscription",
|
label = "Subscription",
|
||||||
|
|
@ -443,7 +462,7 @@ fun SettingsScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSection(title = "Privacy and safety") {
|
SettingsSection(title = "Privacy and safety", accent = Color(0xFFD9B8FF)) {
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
icon = Icons.Filled.Lock,
|
icon = Icons.Filled.Lock,
|
||||||
label = "Security",
|
label = "Security",
|
||||||
|
|
@ -459,7 +478,7 @@ fun SettingsScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSection(title = "Account") {
|
SettingsSection(title = "Account", accent = Color(0xFFFFD9E8)) {
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
icon = Icons.Filled.Warning,
|
icon = Icons.Filled.Warning,
|
||||||
label = "Delete account",
|
label = "Delete account",
|
||||||
|
|
@ -540,7 +559,20 @@ private fun SettingsRow(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
Icon(icon, contentDescription = null, tint = tint)
|
androidx.compose.foundation.layout.Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(14.dp))
|
||||||
|
.background(tint.copy(alpha = if (tint == SettingsDanger) 0.12f else 0.14f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = tint,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,14 @@ struct SettingsView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
List {
|
ScrollView {
|
||||||
Section {
|
VStack(alignment: .leading, spacing: CloserSpacing.lg) {
|
||||||
|
Text("Manage your space, reminders, privacy, and account.")
|
||||||
|
.font(CloserFont.callout)
|
||||||
|
.foregroundColor(.closerTextSecondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.padding(.horizontal, CloserSpacing.xl)
|
||||||
|
|
||||||
if isLoggedInAnonymously {
|
if isLoggedInAnonymously {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
SignUpView()
|
SignUpView()
|
||||||
|
|
@ -26,6 +32,8 @@ struct SettingsView: View {
|
||||||
imageUrl: appState.currentUser?.photoUrl
|
imageUrl: appState.currentUser?.photoUrl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.closerPadding()
|
||||||
} else {
|
} else {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
EditProfileView()
|
EditProfileView()
|
||||||
|
|
@ -37,139 +45,184 @@ struct SettingsView: View {
|
||||||
imageUrl: appState.currentUser?.photoUrl
|
imageUrl: appState.currentUser?.photoUrl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.closerPadding()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Section("For the two of you") {
|
SettingsSectionCard(title: "For the two of you", accent: .closerSecondary) {
|
||||||
if let partner = appState.currentPartner {
|
if let partner = appState.currentPartner {
|
||||||
SettingsLinkLabel(
|
SettingsLinkRow(
|
||||||
icon: "heart.fill",
|
icon: "heart.fill",
|
||||||
title: "Connected with \(partner.displayName.isEmpty ? "Partner" : partner.displayName)",
|
title: "Connected with \(partner.displayName.isEmpty ? "Partner" : partner.displayName)",
|
||||||
subtitle: "Your shared Closer space is active",
|
subtitle: "Your shared Closer space is active",
|
||||||
tint: .closerPrimary,
|
tint: .closerPrimary,
|
||||||
imageUrl: partner.photoUrl
|
imageUrl: partner.photoUrl,
|
||||||
)
|
showsChevron: false
|
||||||
} else {
|
)
|
||||||
Button(action: { isPairingActive = true }) {
|
} else {
|
||||||
SettingsLinkLabel(
|
Button(action: { isPairingActive = true }) {
|
||||||
icon: "heart",
|
SettingsLinkRow(
|
||||||
title: "Invite your partner",
|
icon: "heart",
|
||||||
subtitle: "Start answering together",
|
title: "Invite your partner",
|
||||||
tint: .closerPrimary
|
subtitle: "Start answering together",
|
||||||
|
tint: .closerPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDivider()
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
AnswerHistoryView()
|
||||||
|
} label: {
|
||||||
|
SettingsLinkRow(
|
||||||
|
icon: "checkmark.circle",
|
||||||
|
title: "Answer History",
|
||||||
|
subtitle: "Revisit the moments you have shared"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
.closerPadding()
|
||||||
|
|
||||||
NavigationLink {
|
SettingsSectionCard(title: "Your rhythm", accent: .closerPrimary) {
|
||||||
AnswerHistoryView()
|
NavigationLink {
|
||||||
} label: {
|
NavigationSettingsView()
|
||||||
SettingsLinkLabel(
|
} label: {
|
||||||
icon: "checkmark.circle",
|
SettingsLinkRow(
|
||||||
title: "Answer History",
|
icon: "bell.badge",
|
||||||
subtitle: "Revisit the moments you have shared"
|
title: "Notifications",
|
||||||
)
|
subtitle: "Set gentle reminders that fit your day"
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
Section("Your rhythm") {
|
|
||||||
NavigationLink {
|
|
||||||
NavigationSettingsView()
|
|
||||||
} label: {
|
|
||||||
SettingsLinkLabel(
|
|
||||||
icon: "bell.badge",
|
|
||||||
title: "Notifications",
|
|
||||||
subtitle: "Set gentle reminders that fit your day"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section("Premium") {
|
|
||||||
Button {
|
|
||||||
showPaywall = true
|
|
||||||
} label: {
|
|
||||||
PremiumSettingsCTA()
|
|
||||||
}
|
|
||||||
.disabled(isLoggedInAnonymously)
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section("Privacy and safety") {
|
|
||||||
NavigationLink {
|
|
||||||
DataExportView()
|
|
||||||
} label: {
|
|
||||||
SettingsLinkLabel(
|
|
||||||
icon: "square.and.arrow.up",
|
|
||||||
title: "Export Data",
|
|
||||||
subtitle: "Download a copy of your Closer data"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink {
|
|
||||||
Text("Privacy Policy")
|
|
||||||
} label: {
|
|
||||||
SettingsLinkLabel(
|
|
||||||
icon: "hand.raised",
|
|
||||||
title: "Privacy Policy",
|
|
||||||
subtitle: "How your data is handled"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink {
|
|
||||||
Text("Terms of Service")
|
|
||||||
} label: {
|
|
||||||
SettingsLinkLabel(
|
|
||||||
icon: "doc.text",
|
|
||||||
title: "Terms of Service",
|
|
||||||
subtitle: "The agreement for using Closer"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section("Support") {
|
|
||||||
NavigationLink {
|
|
||||||
HelpCenterView()
|
|
||||||
} label: {
|
|
||||||
SettingsLinkLabel(
|
|
||||||
icon: "questionmark.circle",
|
|
||||||
title: "Help Center",
|
|
||||||
subtitle: "Answers for common questions"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink {
|
|
||||||
Text("Contact Us")
|
|
||||||
} label: {
|
|
||||||
SettingsLinkLabel(
|
|
||||||
icon: "envelope",
|
|
||||||
title: "Contact Us",
|
|
||||||
subtitle: "Get help from the Closer team"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Label("Version", systemImage: "info.circle")
|
|
||||||
Spacer()
|
|
||||||
Text(appVersion)
|
|
||||||
.font(CloserFont.footnote)
|
|
||||||
.foregroundColor(.closerTextSecondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section("Account") {
|
|
||||||
Button(role: .destructive, action: { showLogoutConfirm = true }) {
|
|
||||||
Label("Sign Out", systemImage: "rectangle.portrait.and.arrow.right")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(role: .destructive, action: { showDeleteConfirm = true }) {
|
|
||||||
Label("Delete Account", systemImage: "trash")
|
|
||||||
}
|
}
|
||||||
|
.closerPadding()
|
||||||
|
|
||||||
|
SettingsSectionCard(title: "Premium", accent: .closerSecondary) {
|
||||||
|
Button {
|
||||||
|
showPaywall = true
|
||||||
|
} label: {
|
||||||
|
SettingsLinkRow(
|
||||||
|
icon: "heart.fill",
|
||||||
|
title: "Upgrade to Premium",
|
||||||
|
subtitle: "One subscription for both partners.",
|
||||||
|
tint: .closerPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.disabled(isLoggedInAnonymously)
|
||||||
|
.opacity(isLoggedInAnonymously ? 0.55 : 1)
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
.closerPadding()
|
||||||
|
|
||||||
|
SettingsSectionCard(title: "Privacy and safety", accent: .closerPrimary) {
|
||||||
|
NavigationLink {
|
||||||
|
DataExportView()
|
||||||
|
} label: {
|
||||||
|
SettingsLinkRow(
|
||||||
|
icon: "square.and.arrow.up",
|
||||||
|
title: "Export Data",
|
||||||
|
subtitle: "Download a copy of your Closer data"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
SettingsDivider()
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
Text("Privacy Policy")
|
||||||
|
} label: {
|
||||||
|
SettingsLinkRow(
|
||||||
|
icon: "hand.raised",
|
||||||
|
title: "Privacy Policy",
|
||||||
|
subtitle: "How your data is handled"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
SettingsDivider()
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
Text("Terms of Service")
|
||||||
|
} label: {
|
||||||
|
SettingsLinkRow(
|
||||||
|
icon: "doc.text",
|
||||||
|
title: "Terms of Service",
|
||||||
|
subtitle: "The agreement for using Closer"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
.closerPadding()
|
||||||
|
|
||||||
|
SettingsSectionCard(title: "Support", accent: .closerSecondary) {
|
||||||
|
NavigationLink {
|
||||||
|
HelpCenterView()
|
||||||
|
} label: {
|
||||||
|
SettingsLinkRow(
|
||||||
|
icon: "questionmark.circle",
|
||||||
|
title: "Help Center",
|
||||||
|
subtitle: "Answers for common questions"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
SettingsDivider()
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
Text("Contact Us")
|
||||||
|
} label: {
|
||||||
|
SettingsLinkRow(
|
||||||
|
icon: "envelope",
|
||||||
|
title: "Contact Us",
|
||||||
|
subtitle: "Get help from the Closer team"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
SettingsDivider()
|
||||||
|
|
||||||
|
SettingsVersionRow(version: appVersion)
|
||||||
|
}
|
||||||
|
.closerPadding()
|
||||||
|
|
||||||
|
SettingsSectionCard(title: "Account", accent: .closerDanger.opacity(0.5)) {
|
||||||
|
Button(role: .destructive, action: { showLogoutConfirm = true }) {
|
||||||
|
SettingsLinkRow(
|
||||||
|
icon: "rectangle.portrait.and.arrow.right",
|
||||||
|
title: "Sign Out",
|
||||||
|
subtitle: "Leave this device while keeping your data saved",
|
||||||
|
tint: .closerDanger,
|
||||||
|
isDestructive: true,
|
||||||
|
showsChevron: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
SettingsDivider()
|
||||||
|
|
||||||
|
Button(role: .destructive, action: { showDeleteConfirm = true }) {
|
||||||
|
SettingsLinkRow(
|
||||||
|
icon: "trash",
|
||||||
|
title: "Delete Account",
|
||||||
|
subtitle: "Permanently remove your Closer account",
|
||||||
|
tint: .closerDanger,
|
||||||
|
isDestructive: true,
|
||||||
|
showsChevron: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
.closerPadding()
|
||||||
}
|
}
|
||||||
|
.padding(.top, CloserSpacing.md)
|
||||||
|
.padding(.bottom, CloserSpacing.xxl)
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
|
||||||
.background(Color.closerBackground)
|
.background(Color.closerBackground)
|
||||||
.navigationTitle("Settings")
|
.navigationTitle("Settings")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
.fullScreenCover(isPresented: $showPaywall) {
|
.fullScreenCover(isPresented: $showPaywall) {
|
||||||
PaywallView()
|
PaywallView()
|
||||||
}
|
}
|
||||||
|
|
@ -803,6 +856,46 @@ struct PaywallView: View {
|
||||||
|
|
||||||
// MARK: - Settings Row Helpers
|
// MARK: - Settings Row Helpers
|
||||||
|
|
||||||
|
struct SettingsSectionCard<Content: View>: View {
|
||||||
|
let title: String
|
||||||
|
let accent: Color
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(title: String, accent: Color, @ViewBuilder content: () -> Content) {
|
||||||
|
self.title = title
|
||||||
|
self.accent = accent
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: CloserSpacing.sm) {
|
||||||
|
Text(title)
|
||||||
|
.font(CloserFont.caption)
|
||||||
|
.foregroundColor(.closerTextSecondary)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.background(Color.white.opacity(0.58))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.large, style: .continuous))
|
||||||
|
}
|
||||||
|
.padding(CloserSpacing.md)
|
||||||
|
.background(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color.closerSurface,
|
||||||
|
accent.opacity(0.18)
|
||||||
|
],
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.xlarge, style: .continuous))
|
||||||
|
.closerShadow(level: .small)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct SettingsProfileHeader: View {
|
struct SettingsProfileHeader: View {
|
||||||
let initials: String
|
let initials: String
|
||||||
let name: String
|
let name: String
|
||||||
|
|
@ -829,7 +922,108 @@ struct SettingsProfileHeader: View {
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical)
|
.padding(CloserSpacing.lg)
|
||||||
|
.background(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color.closerSurface,
|
||||||
|
Color.closerSecondary.opacity(0.14)
|
||||||
|
],
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.xlarge, style: .continuous))
|
||||||
|
.closerShadow(level: .small)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsLinkRow: View {
|
||||||
|
let icon: String
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
var tint: Color = .closerTextSecondary
|
||||||
|
var imageUrl: String? = nil
|
||||||
|
var isDestructive: Bool = false
|
||||||
|
var showsChevron: Bool = true
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: CloserSpacing.md) {
|
||||||
|
if let imageUrl, !imageUrl.isEmpty {
|
||||||
|
SettingsAvatarView(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
fallbackIcon: icon,
|
||||||
|
fallbackText: nil,
|
||||||
|
size: 40,
|
||||||
|
tint: tint
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SettingsIconTile(icon: icon, tint: tint, isDestructive: isDestructive)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
Text(title)
|
||||||
|
.font(CloserFont.body)
|
||||||
|
.foregroundColor(isDestructive ? .closerDanger : .closerText)
|
||||||
|
.lineLimit(1)
|
||||||
|
Text(subtitle)
|
||||||
|
.font(CloserFont.caption)
|
||||||
|
.foregroundColor(.closerTextSecondary)
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: CloserSpacing.sm)
|
||||||
|
|
||||||
|
if showsChevron {
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
|
.foregroundColor(.closerTextSecondary.opacity(0.72))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, CloserSpacing.md)
|
||||||
|
.padding(.vertical, CloserSpacing.sm)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsIconTile: View {
|
||||||
|
let icon: String
|
||||||
|
let tint: Color
|
||||||
|
var isDestructive: Bool = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||||
|
.fill(tint.opacity(isDestructive ? 0.12 : 0.14))
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(tint)
|
||||||
|
}
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsDivider: View {
|
||||||
|
var body: some View {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.closerDivider.opacity(0.8))
|
||||||
|
.frame(height: 1)
|
||||||
|
.padding(.leading, 64)
|
||||||
|
.padding(.trailing, CloserSpacing.md)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsVersionRow: View {
|
||||||
|
let version: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
SettingsLinkRow(
|
||||||
|
icon: "info.circle",
|
||||||
|
title: "Version",
|
||||||
|
subtitle: version,
|
||||||
|
showsChevron: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue