Move diagnostics into settings, remove the app-local notification module, unify the global theme switcher, refine privacy copy and the instance detail experience, and rebuild the app icon.
149 lines
4.5 KiB
Swift
149 lines
4.5 KiB
Swift
import SwiftUI
|
|
|
|
struct SettingsView: View {
|
|
@StateObject private var model: SettingsViewModel
|
|
@ObservedObject private var themeStore = ThemeStore.shared
|
|
@State private var showsClearDialog = false
|
|
|
|
init(model: SettingsViewModel? = nil) {
|
|
_model = StateObject(wrappedValue: model ?? DefaultContainer.makeSettingsModel())
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
LazyVStack(spacing: 12) {
|
|
if let errorMessage = model.errorMessage {
|
|
AIStatusBanner(
|
|
errorMessage, tint: AITheme.danger, systemImage: "exclamationmark.triangle")
|
|
}
|
|
if let notice = model.notice {
|
|
AIStatusBanner(notice, tint: AITheme.success, systemImage: "checkmark.circle")
|
|
}
|
|
|
|
runtimeCard
|
|
themeCard
|
|
storageCard
|
|
securityCard
|
|
dangerZoneCard
|
|
}
|
|
.padding(16)
|
|
.padding(.bottom, 8)
|
|
}
|
|
.background(AITheme.canvas.ignoresSafeArea())
|
|
.navigationTitle("设置")
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .primaryAction) {
|
|
AIToolbarButton(
|
|
"刷新",
|
|
systemImage: "arrow.clockwise",
|
|
kind: .primary
|
|
) {
|
|
Task { await model.refresh() }
|
|
}
|
|
.disabled(model.isLoading)
|
|
}
|
|
}
|
|
.refreshable {
|
|
await model.refresh()
|
|
}
|
|
.confirmationDialog(
|
|
"确认清除运行日志?此操作不可恢复。",
|
|
isPresented: $showsClearDialog,
|
|
titleVisibility: .visible
|
|
) {
|
|
Button("清除运行日志", role: .destructive) {
|
|
Task { await model.clearRuntimeLogs() }
|
|
}
|
|
Button("取消", role: .cancel) {}
|
|
}
|
|
.task {
|
|
await model.refresh()
|
|
}
|
|
}
|
|
|
|
private var runtimeCard: some View {
|
|
AICard {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
AISectionTitle("应用信息", systemImage: "info.circle")
|
|
if let runtime = model.runtime {
|
|
AIKeyValueRow("版本", value: "\(runtime.marketingVersion) (\(runtime.buildVersion))")
|
|
AIKeyValueRow("设备", value: runtime.deviceModel)
|
|
} else {
|
|
Text("正在读取应用信息…")
|
|
.font(.subheadline)
|
|
.foregroundStyle(AITheme.muted)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var themeCard: some View {
|
|
AICard {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
AISectionTitle("外观", systemImage: "circle.lefthalf.filled", detail: themeStore.mode.label)
|
|
AISegmentedControl(
|
|
"主题模式",
|
|
selection: Binding(
|
|
get: { themeStore.mode },
|
|
set: { themeStore.set($0) }
|
|
),
|
|
options: ThemeMode.allCases.map { (value: $0, title: $0.label) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var storageCard: some View {
|
|
AICard {
|
|
NavigationLink {
|
|
LogsView()
|
|
} label: {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack(spacing: 10) {
|
|
AISectionTitle("本地数据", systemImage: "externaldrive")
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
Image(systemName: "chevron.right")
|
|
.font(.footnote.weight(.semibold))
|
|
.foregroundStyle(AITheme.muted)
|
|
.accessibilityHidden(true)
|
|
}
|
|
AIKeyValueRow("数据位置", value: "仅保存在本机设备")
|
|
AIKeyValueRow("当前占用", value: model.storageLabel)
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityHint("进入日志与诊断")
|
|
}
|
|
}
|
|
|
|
private var securityCard: some View {
|
|
AICard {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
AISectionTitle("隐私与数据", systemImage: "lock.shield")
|
|
AIKeyValueRow("节点信息", value: "仅保存在本机设备")
|
|
AIKeyValueRow("实例密码", value: "保存在本地数据文件中")
|
|
AIKeyValueRow("登录会话", value: "应用关闭后自动清除")
|
|
AIStatusBanner(
|
|
"节点地址、实例密码和运行数据都保存在本机。应用只连接你添加的节点,登录会话关闭应用后即被清除。",
|
|
tint: AITheme.primary,
|
|
systemImage: "shield.checkered"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var dangerZoneCard: some View {
|
|
AICard {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
AISectionTitle("数据清理", systemImage: "trash.square", detail: "不可恢复")
|
|
Button {
|
|
showsClearDialog = true
|
|
} label: {
|
|
Label("清除运行日志", systemImage: "trash")
|
|
}
|
|
.buttonStyle(AIButtonStyle(kind: .danger, size: .regular, fillsWidth: true))
|
|
}
|
|
}
|
|
}
|
|
}
|