import SwiftUI /// 设置容器(首页齿轮 → sheet)。过渡形态:`NavigationStack` + 分类跳转。 /// UI-5 将按设计稿 `1g 设置-外观` 重写为「左 280pt 类别栏 + 右内容区」。 struct SettingsHost: View { @ObservedObject var credStore: FileCredentialStore @ObservedObject var tailscaleStore: TailscaleStore @Environment(\.dismiss) private var dismiss @EnvironmentObject var theme: TXThemeManager /// known hosts 是无状态的 keychain/文件访问层,容器自持一个即可。 private let knownHosts = KeychainKnownHostsStore() private var p: TXPalette { theme.palette } var body: some View { NavigationStack { ZStack { p.bg.ignoresSafeArea() List { Section("外观") { NavigationLink { SettingsView() } label: { row("paintbrush", "外观与主题") } } .listRowBackground(p.surface) Section("保险库") { NavigationLink { KeychainListView(store: credStore) } label: { row("key.fill", "Keychain 凭据") } NavigationLink { KnownHostsView(store: knownHosts) } label: { row("checkmark.shield", "Known Hosts") } } .listRowBackground(p.surface) Section("网络") { NavigationLink { TailscaleListView(store: tailscaleStore) } label: { row("network", "Tailscale") } } .listRowBackground(p.surface) } .scrollContentBackground(.hidden) .foregroundStyle(p.text) } .navigationTitle("设置") .toolbarBackground(p.bg, for: .navigationBar) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("完成") { dismiss() }.tint(TXAccent.base) } } } .tint(TXAccent.base) } private func row(_ icon: String, _ title: String) -> some View { Label(title, systemImage: icon) .font(TX.Font.mono(13)) .foregroundStyle(p.text) } } /// 设置页:主题(app 与终端统一)+ 终端 + 关于。 struct SettingsView: View { @EnvironmentObject var theme: TXThemeManager private var p: TXPalette { theme.palette } var body: some View { ZStack { p.bg.ignoresSafeArea() List { Section("主题(app 与终端统一)") { ForEach(TXPalette.all) { pal in Button { theme.apply(pal) } label: { themeRow(pal) } .buttonStyle(.txPress) } } .listRowBackground(p.surface) Section("终端") { row("textformat", "字体与大小", value: "系统等宽") } .listRowBackground(p.surface) Section("关于") { row("info.circle", "terminalX", value: "0.0.1") } .listRowBackground(p.surface) } .scrollContentBackground(.hidden) .foregroundStyle(p.textPrimary) } .navigationTitle("设置") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(p.bg, for: .navigationBar) } /// 主题行:色板预览(底/面/强调)+ 名称 + 选中勾。 private func themeRow(_ pal: TXPalette) -> some View { HStack(spacing: TX.Space.m) { ZStack { RoundedRectangle(cornerRadius: 7).fill(pal.bg) HStack(spacing: 3) { Circle().fill(pal.accent).frame(width: 7, height: 7) Circle().fill(pal.mosh).frame(width: 7, height: 7) } } .frame(width: 40, height: 28) .overlay(RoundedRectangle(cornerRadius: 7).stroke(p.border, lineWidth: 1)) Text(pal.name).foregroundStyle(p.textPrimary) Spacer() if pal.id == theme.palette.id { Image(systemName: "checkmark").foregroundStyle(p.accent).font(.system(size: 14, weight: .semibold)) } } } private func row(_ icon: String, _ title: String, value: String) -> some View { HStack { Image(systemName: icon).foregroundStyle(p.textSecondary).frame(width: 26) Text(title).foregroundStyle(p.textPrimary) Spacer() Text(value).foregroundStyle(p.textSecondary).font(.system(size: 14)) } } } /// 已信任 host key(TOFU pin)查看/删除页。 struct KnownHostsView: View { let store: KeychainKnownHostsStore @EnvironmentObject var theme: TXThemeManager @State private var entries: [KnownHostEntry] = [] private var p: TXPalette { theme.palette } var body: some View { ZStack { p.bg.ignoresSafeArea() ScrollView { VStack(alignment: .leading, spacing: TX.Space.s) { if entries.isEmpty { TXEmptyState(icon: "checkmark.shield", title: "还没有已信任的主机密钥", subtitle: "首次连接主机时会自动记住其 host key") } else { ForEach(entries) { e in row(e) .contextMenu { Button(role: .destructive) { store.removeKey(e.id); reload() } label: { Label("移除信任", systemImage: "trash") } } } } } .padding(TX.Space.m) } } .navigationTitle("Known Hosts") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(p.bg, for: .navigationBar) .onAppear(perform: reload) } private func row(_ e: KnownHostEntry) -> some View { VStack(alignment: .leading, spacing: 3) { Text("\(e.host):\(e.port)").font(.system(size: 15, weight: .semibold)).foregroundStyle(p.textPrimary) Text("出口:\(e.egress)").font(TX.Font.mono(11)).foregroundStyle(p.textSecondary) Text(e.fingerprint).font(TX.Font.mono(11)).foregroundStyle(p.textTertiary) .textSelection(.enabled) } .frame(maxWidth: .infinity, alignment: .leading) .padding(TX.Space.m) .background(p.surface, in: RoundedRectangle(cornerRadius: TX.Radius.card)) .overlay(RoundedRectangle(cornerRadius: TX.Radius.card).stroke(p.border, lineWidth: 1)) } private func reload() { entries = store.entries() } }