侧边栏(真机验证): - 砍掉悬浮态,只留收起⇄固定展开;TerminalSidebar 单一视图身份, 内容恒按 288pt 排版、收起=左侧裁切(clipped 宽度动画), 图标列两态同尺寸同位置,明细淡入淡出,终端随宽度盖上/让开 - 开合入口收敛:Logo 位(收起态=»展开键/展开态=Logo)+ 头部«; 删底部开合键与 Pin;LeadingHitShape 修 clipped 不裁触摸 - 修 GridBox 从不通知 UI:onGridChange→objectWillChange, 标题胶囊格子数随开合实时刷新 触控化走查(上一轮,真机部署验证): - 窗口按钮 13pt 色点→22pt 圆角方块常显字形;动作条改可点按钮 - TXTech 拼写/配色规范(SSH/mosh/tmux 官方写法+品牌色映射) - 首页会话卡固定 3 列 4:3;去掉未实现的 ⌘K 提示 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
174 lines
6.9 KiB
Swift
174 lines
6.9 KiB
Swift
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() }
|
||
}
|