Files
edgeever/apps/ios/EdgeEver/Features/Workspace/SettingsView.swift
T

788 lines
33 KiB
Swift

import SwiftUI
/// Android `WorkspaceSettingsView` parity: full-screen “我的”, not a system Form/List.
struct SettingsView: View {
@Environment(AppEnvironment.self) private var env
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
private enum RootTab: Hashable {
case general
case account
case system
// Extended (not on Android root Me, but reachable from system/account extras)
case tags
case tokens
case devices
case users
}
@State private var tab: RootTab?
@State private var showLocalePicker = false
@State private var copiedSystemInfo = false
@State private var instanceHealth: InstanceHealth?
@State private var instanceVersion: String?
@State private var instanceLatencyMilliseconds: Int?
@State private var instanceDiagnosticsFailed = false
private var title: String {
switch tab {
case .general: return env.preferences.t("常规设置", en: "General")
case .account: return env.preferences.t("登录设置", en: "Account")
case .system: return env.preferences.t("系统信息", en: "System info")
case .tags: return env.preferences.t("标签管理", en: "Tags")
case .tokens: return "API Token"
case .devices: return env.preferences.t("登录设备", en: "Devices")
case .users: return env.preferences.t("用户管理", en: "Users")
case nil: return env.preferences.t("我的", en: "Me")
}
}
private var headerIcon: String {
switch tab {
case .general: return "slider.horizontal.3"
case .account: return "shield.checkered"
case .system: return "info.circle"
case .tags: return "tag"
case .tokens: return "key"
case .devices: return "iphone"
case .users: return "person.2"
case nil: return "person"
}
}
var body: some View {
VStack(spacing: 0) {
settingsHeader
ScrollView {
Group {
if let tab {
detailContent(tab)
.transition(.opacity.combined(with: .move(edge: .trailing)))
} else {
rootMenu
.transition(.opacity)
}
}
.padding(16)
.padding(.bottom, 96)
.animation(Motion.listContent, value: tab)
}
.background(AppTheme.background)
}
.background(AppTheme.background.ignoresSafeArea())
.preferredColorScheme(env.preferences.colorScheme)
.task(id: tab) {
guard tab == .system else { return }
await loadInstanceDiagnostics()
}
}
// MARK: - Header (Android settingsHeader)
private var settingsHeader: some View {
HStack(spacing: 0) {
Button {
if tab != nil {
withAnimation(Motion.chip) { tab = nil }
} else {
dismiss()
}
} label: {
Image(systemName: "chevron.left")
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(AppTheme.secondary)
.frame(width: 36, height: 36)
}
.buttonStyle(.plain)
HStack(spacing: 8) {
Image(systemName: headerIcon)
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(AppTheme.accentStrong)
Text(title)
.font(.system(size: 16, weight: .heavy))
.foregroundStyle(AppTheme.title)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 4)
Button {
withAnimation(Motion.chip) {
env.preferences.theme = resolvedDarkMode ? "light" : "dark"
}
} label: {
HStack(spacing: 8) {
Image(systemName: resolvedDarkMode ? "sun.max" : "moon")
.font(.system(size: 16, weight: .semibold))
Text(themeToggleLabel)
.font(.system(size: 14, weight: .bold))
.lineLimit(1)
}
.foregroundStyle(AppTheme.slate)
.padding(.horizontal, 8)
.frame(height: 36)
}
.buttonStyle(.plain)
}
.padding(.horizontal, 12)
.frame(minHeight: 56)
.background(AppTheme.card)
.overlay(alignment: .bottom) {
Rectangle().fill(AppTheme.border).frame(height: 1)
}
}
private var themeToggleLabel: String {
// Android shows the *action* text: switch to light when dark, else switch to dark
if resolvedDarkMode {
return env.preferences.t("切换到浅色模式", en: "Light mode")
}
return env.preferences.t("切换到深色模式", en: "Dark mode")
}
private var resolvedDarkMode: Bool {
env.preferences.theme == "dark" || (env.preferences.theme == "system" && colorScheme == .dark)
}
// MARK: - Root Me menu (Android activeTab === null)
private var rootMenu: some View {
VStack(spacing: 16) {
// Card 1: general + account
settingsMenuCard {
menuRow(
icon: "slider.horizontal.3",
iconTint: AppTheme.accent,
iconBg: AppTheme.accentSoft,
title: env.preferences.t("常规设置", en: "General"),
showBorder: false
) {
withAnimation(Motion.chip) { tab = .general }
}
menuRow(
icon: "shield.checkered",
iconTint: AppTheme.accent,
iconBg: AppTheme.accentSoft,
title: env.preferences.t("登录设置", en: "Account"),
showBorder: true
) {
withAnimation(Motion.chip) { tab = .account }
}
}
// Card 2: system + feedback
settingsMenuCard {
menuRow(
icon: "info.circle",
iconTint: AppTheme.accent,
iconBg: AppTheme.accentSoft,
title: env.preferences.t("系统信息", en: "System info"),
subtitle: env.preferences.t(
"查看版本与运行环境信息。",
en: "View version and runtime info."
),
showBorder: false
) {
withAnimation(Motion.chip) { tab = .system }
}
menuRow(
icon: "bubble.left.and.bubble.right",
iconTint: AppTheme.secondary,
iconBg: AppTheme.searchFill,
title: env.preferences.t("意见反馈", en: "Feedback"),
subtitle: env.preferences.t(
"报告问题或提出功能建议",
en: "Report issues or suggest features"
),
trailing: .external,
showBorder: true
) {
if let url = feedbackURL { UIApplication.shared.open(url) }
}
}
}
}
private enum TrailingKind { case chevron, external }
private func settingsMenuCard<Content: View>(@ViewBuilder _ content: () -> Content) -> some View {
VStack(spacing: 0) {
content()
}
.background(AppTheme.card)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.stroke(AppTheme.border, lineWidth: 1)
)
}
private func menuRow(
icon: String,
iconTint: Color,
iconBg: Color,
title: String,
subtitle: String? = nil,
trailing: TrailingKind = .chevron,
showBorder: Bool,
action: @escaping () -> Void
) -> some View {
Button(action: action) {
HStack(spacing: 12) {
ZStack {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(iconBg)
.frame(width: 32, height: 32)
Image(systemName: icon)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(iconTint)
}
VStack(alignment: .leading, spacing: 1) {
Text(title)
.font(.system(size: 14, weight: .bold))
.foregroundStyle(AppTheme.title)
if let subtitle {
Text(subtitle)
.font(.system(size: 12))
.foregroundStyle(AppTheme.secondary)
.lineLimit(2)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: trailing == .external ? "arrow.up.right" : "chevron.right")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(AppTheme.muted)
}
.padding(.horizontal, 16)
.frame(minHeight: 64)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.overlay(alignment: .top) {
if showBorder {
Rectangle().fill(AppTheme.cardBorder).frame(height: 1)
.padding(.leading, 16)
}
}
}
// MARK: - Detail tabs
@ViewBuilder
private func detailContent(_ tab: RootTab) -> some View {
switch tab {
case .general:
generalContent
case .account:
accountContent
case .system:
systemContent
case .tags:
TagsManagementView()
case .tokens:
ApiTokensView()
case .devices:
DevicesView()
case .users:
UsersManagementView()
}
}
private var generalContent: some View {
VStack(spacing: 16) {
settingsGroup(
title: env.preferences.t("偏好设置", en: "Preferences"),
icon: "photo"
) {
preferenceBlock(
title: env.preferences.t("界面语言", en: "Language"),
description: env.preferences.t("切换产品界面的显示语言。", en: "Switch the product UI language.")
) {
Menu {
Button(env.preferences.t("跟随系统", en: "System")) { env.preferences.localeCode = "system" }
Button("简体中文") { env.preferences.localeCode = "zh-CN" }
Button("English") { env.preferences.localeCode = "en-US" }
} label: {
HStack {
Text(localeLabel)
.font(.system(size: 14))
.foregroundStyle(AppTheme.title)
Spacer()
Image(systemName: "chevron.down")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(AppTheme.secondary)
}
.padding(.horizontal, 12)
.frame(minHeight: 40)
.background(AppTheme.card)
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8).stroke(AppTheme.border, lineWidth: 1)
)
}
}
preferenceBlock(
title: env.preferences.t("压缩笔记内图片", en: "Compress note images"),
description: env.preferences.t(
"上传前将大图压缩为 WebP(最长边 2560),节省存储与流量。",
en: "Compress large images to WebP (max edge 2560) before upload to save storage and bandwidth."
),
showTopBorder: true
) {
Toggle("", isOn: Bindable(env.preferences).useCompression)
.labelsHidden()
.tint(AppTheme.accent)
.frame(maxWidth: .infinity, alignment: .leading)
}
// List density lives in list-options sheet (Android NotesActionsModal), not here.
}
}
}
private var accountContent: some View {
VStack(spacing: 16) {
// Account summary card
HStack(spacing: 12) {
ZStack {
Circle().fill(AppTheme.accentSoft).frame(width: 40, height: 40)
Image(systemName: "person")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(AppTheme.accentStrong)
}
VStack(alignment: .leading, spacing: 2) {
Text(env.preferences.t("当前账户", en: "Current account"))
.font(.system(size: 12, weight: .bold))
.foregroundStyle(AppTheme.secondary)
Text(env.session.session?.user?.displayName ?? env.session.session?.user?.username ?? "—")
.font(.system(size: 15, weight: .heavy))
.foregroundStyle(AppTheme.title)
let role = env.session.session?.user?.role == "owner"
? env.preferences.t("实例管理员", en: "Owner")
: env.preferences.t("成员", en: "Member")
Text("@\(env.session.session?.user?.username ?? "—") · \(role)")
.font(.system(size: 12))
.foregroundStyle(AppTheme.secondary)
.padding(.top, 1)
}
Spacer(minLength: 0)
}
.padding(16)
.background(AppTheme.card)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(AppTheme.border, lineWidth: 1)
)
// Change password panel (Android AccountSecurityPanel)
AccountPasswordPanel()
// Extended links (tokens/devices/users/tags) — compact rows, not on Android root Me
settingsMenuCard {
menuRow(icon: "iphone", iconTint: AppTheme.accent, iconBg: AppTheme.accentSoft,
title: env.preferences.t("登录设备", en: "Devices"), showBorder: false) {
withAnimation(Motion.chip) { tab = .devices }
}
menuRow(icon: "key", iconTint: AppTheme.accent, iconBg: AppTheme.accentSoft,
title: "API Token", showBorder: true) {
withAnimation(Motion.chip) { tab = .tokens }
}
menuRow(icon: "tag", iconTint: AppTheme.accent, iconBg: AppTheme.accentSoft,
title: env.preferences.t("标签管理", en: "Tags"), showBorder: true) {
withAnimation(Motion.chip) { tab = .tags }
}
if env.session.session?.user?.role == "owner" {
menuRow(icon: "person.2", iconTint: AppTheme.accent, iconBg: AppTheme.accentSoft,
title: env.preferences.t("用户管理", en: "Users"), showBorder: true) {
withAnimation(Motion.chip) { tab = .users }
}
}
}
// Logout card
VStack(alignment: .leading, spacing: 0) {
Button {
Task {
await env.session.signOut()
dismiss()
}
} label: {
HStack(spacing: 8) {
Image(systemName: "rectangle.portrait.and.arrow.right")
.font(.system(size: 15, weight: .bold))
Text(env.preferences.t("退出登录", en: "Sign out"))
.font(.system(size: 14, weight: .heavy))
}
.foregroundStyle(.white)
.padding(.horizontal, 14)
.frame(minHeight: 40)
.background(AppTheme.dangerAction)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
.buttonStyle(.plain)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(AppTheme.dangerSurface)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.stroke(AppTheme.dangerBorder, lineWidth: 1)
)
}
}
private var systemContent: some View {
VStack(spacing: 16) {
settingsGroup(title: env.preferences.t("系统信息", en: "System info"), icon: "info.circle") {
Button {
UIPasteboard.general.string = systemInfoText
copiedSystemInfo = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) { copiedSystemInfo = false }
} label: {
HStack(spacing: 8) {
Image(systemName: copiedSystemInfo ? "checkmark.shield" : "doc.on.doc")
Text(copiedSystemInfo
? env.preferences.t("已复制", en: "Copied")
: env.preferences.t("复制信息", en: "Copy info"))
.font(.system(size: 14, weight: .bold))
Spacer()
}
.foregroundStyle(copiedSystemInfo ? AppTheme.accentStrong : AppTheme.title)
.padding(.horizontal, 16)
.frame(minHeight: 44)
}
.buttonStyle(.plain)
}
systemInfoGroup(
title: env.preferences.t("云端实例", en: "Cloud instance"),
description: env.preferences.t("当前连接实例的版本与部署环境。", en: "Version and deployment environment for the connected instance."),
icon: "cloud",
items: cloudSystemInfoItems
)
systemInfoGroup(
title: env.preferences.t("当前客户端", en: "Current client"),
description: env.preferences.t("这台设备上的 EdgeEver 应用与运行环境。", en: "The EdgeEver app and runtime environment on this device."),
icon: "iphone",
items: clientSystemInfoItems
)
systemInfoGroup(
title: env.preferences.t("连接与同步", en: "Connection and sync"),
description: env.preferences.t("实例连接与本地同步队列状态。", en: "Connection and local sync queue status."),
icon: "arrow.triangle.2.circlepath",
items: connectionSystemInfoItems
)
Button {
Task { await env.runSyncCycle() }
} label: {
HStack {
Image(systemName: "arrow.clockwise")
Text(env.preferences.t("立即同步", en: "Sync now"))
.font(.system(size: 14, weight: .bold))
if env.isSyncing { ProgressView() }
}
.frame(maxWidth: .infinity)
.frame(minHeight: 44)
.foregroundStyle(AppTheme.title)
.background(AppTheme.card)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(AppTheme.border, lineWidth: 1))
}
.buttonStyle(.plain)
if let err = env.lastSyncError {
Text(err)
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(AppTheme.danger)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
// MARK: - Building blocks
private var localeLabel: String {
switch env.preferences.localeCode {
case "zh-CN": return "简体中文"
case "en-US": return "English"
default: return env.preferences.t("跟随系统", en: "System")
}
}
private var clientSystemInfoItems: [(label: String, value: String)] {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "—"
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "—"
let language = env.preferences.localeCode == "system"
? "\(env.preferences.resolvedLocale.identifier) (\(env.preferences.t("跟随系统", en: "Follow system")))"
: env.preferences.resolvedLocale.identifier
return [
(env.preferences.t("版本", en: "Version"), "v\(version)"),
(env.preferences.t("构建", en: "Build"), build),
(env.preferences.t("客户端", en: "Client"), env.preferences.t("移动应用", en: "Mobile app")),
(env.preferences.t("系统", en: "System"), "iOS"),
(env.preferences.t("系统版本", en: "System version"), UIDevice.current.systemVersion),
(env.preferences.t("语言", en: "Language"), language),
(env.preferences.t("时区", en: "Time zone"), TimeZone.current.identifier),
(env.preferences.t("安装形态", en: "Mode"), env.preferences.t("原生 SwiftUI 应用", en: "Native SwiftUI app")),
]
}
private var cloudSystemInfoItems: [(label: String, value: String)] {
var items: [(label: String, value: String)] = [
(env.preferences.t("实例版本", en: "Instance version"), instanceVersion.map { "v\($0.replacingOccurrences(of: "^v", with: "", options: .regularExpression))" } ?? unknownSystemInfoValue),
(env.preferences.t("实例构建", en: "Instance build"), instanceHealth?.build ?? unknownSystemInfoValue),
(env.preferences.t("数据库版本", en: "Database version"), instanceHealth?.migration ?? unknownSystemInfoValue),
(env.preferences.t("数据库后端", en: "Database backend"), databaseBackendLabel(instanceHealth?.storage?.database)),
(env.preferences.t("新上传对象存储", en: "New upload object storage"), objectStorageLabel(instanceHealth)),
]
if instanceHealth?.objectStorageProvider == "s3" {
items.append((
env.preferences.t("已有附件", en: "Existing attachments"),
env.preferences.t("继续从原存储读取", en: "Read from original storage")
))
}
items.append((
env.preferences.t("部署平台", en: "Deployment platform"),
deploymentPlatformLabel(instanceHealth?.runtime)
))
if instanceHealth?.runtime == "self-hosted-bun" {
items.append((
env.preferences.t("容器镜像来源", en: "Container image source"),
containerImageSourceLabel(instanceHealth?.containerImageSource)
))
}
return items
}
private var connectionSystemInfoItems: [(label: String, value: String)] {
let queueItems = env.session.dataScope.flatMap { try? env.outbox.listItems(scope: $0) } ?? []
let pending = queueItems.filter { $0.status == .pending || $0.status == .syncing }.count
let failed = queueItems.filter { $0.status == .error || $0.status == .conflict }.count
let connection = instanceHealth != nil
? env.preferences.t("连接正常", en: "Connected")
: instanceDiagnosticsFailed
? env.preferences.t("连接失败", en: "Connection failed")
: env.preferences.t("正在检查", en: "Checking")
return [
(env.preferences.t("实例连接", en: "Instance connection"), connection),
(env.preferences.t("请求耗时", en: "Request latency"), instanceLatencyMilliseconds.map { "\($0) ms" } ?? unknownSystemInfoValue),
(env.preferences.t("待同步", en: "Pending sync"), String(pending)),
(env.preferences.t("失败或冲突", en: "Failed or conflicted"), String(failed)),
]
}
private var unknownSystemInfoValue: String {
env.preferences.t("未知", en: "Unknown")
}
private func databaseBackendLabel(_ backend: String?) -> String {
switch backend {
case "d1": return "D1"
case "sqlite": return "SQLite"
case let value?: return value
case nil: return unknownSystemInfoValue
}
}
private func objectStorageLabel(_ health: InstanceHealth?) -> String {
if health?.objectStorageProvider == "s3" {
return env.preferences.t("第三方 S3 兼容 OSS", en: "Third-party S3-compatible OSS")
}
guard health?.objectStorageProvider == "builtin" else { return unknownSystemInfoValue }
switch health?.storage?.resources {
case "r2": return env.preferences.t("内置 R2", en: "Built-in R2")
case "filesystem": return env.preferences.t("本地文件系统", en: "Local filesystem")
case "s3": return env.preferences.t("实例内置 S3 兼容存储", en: "Instance-provided S3-compatible storage")
default: return unknownSystemInfoValue
}
}
private func deploymentPlatformLabel(_ runtime: String?) -> String {
switch runtime {
case "cloudflare-workers": return "Cloudflare"
case "self-hosted-bun": return "Docker"
default: return unknownSystemInfoValue
}
}
private func containerImageSourceLabel(_ source: String?) -> String {
switch source {
case "official-ghcr": return "GitHub Container Registry (GHCR)"
case "official-cn-mirror": return env.preferences.t("中国大陆官方镜像", en: "Official mainland China mirror")
case "custom": return env.preferences.t("自定义镜像", en: "Custom image")
default: return unknownSystemInfoValue
}
}
private var systemInfoText: String {
[
systemInfoTextSection(env.preferences.t("云端实例", en: "Cloud instance"), items: cloudSystemInfoItems),
systemInfoTextSection(env.preferences.t("当前客户端", en: "Current client"), items: clientSystemInfoItems),
systemInfoTextSection(env.preferences.t("连接与同步", en: "Connection and sync"), items: connectionSystemInfoItems),
].joined(separator: "\n\n")
}
private func systemInfoTextSection(_ title: String, items: [(label: String, value: String)]) -> String {
([title] + items.map { "\($0.label): \($0.value)" }).joined(separator: "\n")
}
private func loadInstanceDiagnostics() async {
guard env.session.isSignedIn else {
instanceDiagnosticsFailed = true
return
}
instanceDiagnosticsFailed = false
let startedAt = Date()
do {
async let health = env.session.client.getInstanceHealth()
async let release = env.session.client.getInstanceRelease()
let (resolvedHealth, resolvedRelease) = try await (health, release)
instanceHealth = resolvedHealth
instanceVersion = resolvedRelease.version
instanceLatencyMilliseconds = max(0, Int(Date().timeIntervalSince(startedAt) * 1_000))
} catch {
instanceHealth = nil
instanceVersion = nil
instanceLatencyMilliseconds = nil
instanceDiagnosticsFailed = true
}
}
private var feedbackURL: URL? {
let english = env.preferences.isEnglish
let heading = english ? "Feedback" : "反馈内容"
let prompt = english
? "Describe the problem, steps to reproduce it, or the feature you would like to see."
: "请描述遇到的问题、复现步骤,或你希望增加的功能。"
let privacy = english
? "GitHub Issues are public. Do not include passwords, tokens, instance URLs, or private note content."
: "GitHub Issue 公开可见,请勿提交密码、Token、实例地址或私人笔记内容。"
let infoHeading = english ? "System information" : "系统信息"
let notice = english
? "The following information was generated by EdgeEver to help diagnose the issue."
: "以下信息由 EdgeEver 自动生成,可帮助定位问题。"
let body = """
## \(heading)
\(prompt)
> \(privacy)
## \(infoHeading)
\(notice)
```text
\(systemInfoText)
```
"""
var components = URLComponents(string: "https://github.com/tianma-if/edgeever/issues/new")
components?.queryItems = [
URLQueryItem(name: "title", value: english ? "[Feedback] " : "[反馈] "),
URLQueryItem(name: "body", value: body),
]
return components?.url
}
private func settingsGroup<Content: View>(
title: String,
icon: String,
@ViewBuilder content: () -> Content
) -> some View {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 8) {
Image(systemName: icon)
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(AppTheme.accentStrong)
Text(title)
.font(.system(size: 14, weight: .heavy))
.foregroundStyle(AppTheme.title)
}
.padding(16)
content()
}
.background(AppTheme.card)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(AppTheme.border, lineWidth: 1)
)
}
private func systemInfoGroup(
title: String,
description: String,
icon: String,
items: [(label: String, value: String)]
) -> some View {
settingsGroup(title: title, icon: icon) {
Text(description)
.font(.system(size: 12))
.foregroundStyle(AppTheme.secondary)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
.padding(.bottom, 12)
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
infoRow(item.label, item.value, showBorder: true)
}
}
}
private func preferenceBlock<Content: View>(
title: String,
description: String,
showTopBorder: Bool = false,
@ViewBuilder content: () -> Content
) -> some View {
VStack(alignment: .leading, spacing: 10) {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.system(size: 14, weight: .bold))
.foregroundStyle(AppTheme.title)
Text(description)
.font(.system(size: 12))
.foregroundStyle(AppTheme.secondary)
.fixedSize(horizontal: false, vertical: true)
}
content()
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.overlay(alignment: .top) {
if showTopBorder {
Rectangle().fill(AppTheme.cardBorder).frame(height: 1)
}
}
}
private func infoRow(_ title: String, _ value: String, showBorder: Bool) -> some View {
HStack(alignment: .top) {
Text(title)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(AppTheme.secondary)
Spacer()
Text(value)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(AppTheme.title)
.multilineTextAlignment(.trailing)
}
.padding(16)
.overlay(alignment: .top) {
if showBorder {
Rectangle().fill(AppTheme.cardBorder).frame(height: 1)
}
}
}
}
// MARK: - Password panel (Android AccountSecurityPanel)